3

I am trying to work with ReSwift in my ios project and had a question regarding how to deal with changes in my view. I am finding that I need to know what the old state was before I can apply changes proposed by the new state coming in. I never needed to know what my old state was while working with redux in my react pojects.

My particular use case is, I am bulding a CameraView with an Overlay screen. From anywhere in the app say a ViewController I can create a CameraView and trigger it to open an UIImagePickerController from within it by firing an action. Here's some code:

//ViewController:

class MainViewController: UIViewController {
    var cameraView: CameraView?
    @IBOutlet weak var launchCameraButton: UIButton!

    init() {
        cameraView = CameraView(self)
    }

    @IBAction func launchCameraButtonClicked(_ sender: Any) {
       store.dispatch(OpenCameraAction())
    }

}

//CameraActions
struct OpenCameraAction: Action {}
struct CloseCameraAction: Action {}

//CameraState
struct CameraState {
    var cameraIsVisible: Bool
}

func cameraReducer(state: CameraState?, action: Action) -> CameraState {
    let initialState = state ?? CameraState()

    switch action {
        case _ as OpenCameraAction:
            return CameraState(cameraIsVisible: true)
        default:
            return initialState
    }    
}

//CameraView

class CameraView: StoreSubscriber {
    private var imagePicker: UIImagePickerController?
    weak private var viewController: UIViewController?

    init(viewController: UIViewController) {
        self.viewController = viewController
        super.init()
        imagePicker = UIImagePickerController()
        imagePicker?.allowsEditing = true
        imagePicker?.sourceType = .camera
        imagePicker?.cameraCaptureMode = .photo
        imagePicker?.cameraDevice = .rear
        imagePicker?.modalPresentationStyle = .fullScreen
        imagePicker?.delegate = self
        imagePicker?.showsCameraControls = false

        store.subscribe(self) { subscription in
           subscription.select { state in
                state.camera
           }
        }
    }

    func newState(state: CameraState?) {
        guard let state = state else {
            return
        }
        if state.cameraIsVisible {
            self.open()
        } else if !state.cameraIsVisible {
            self.close()
        }
    }

    func open() {
        if let imagePicker = self.imagePicker {
            self.viewController?.present(
                imagePicker,
                animated: true
            )
        }
    }

    func close(){
         self.imagePicker?.dismiss(animated: true)
    }

}

The above is all the code to open and close the camera. My confusion starts, when we add more actions, such as disable or enable flash. I need to tack on additional state transitions in my view.

My actions now grow to:

struct OpenCameraAction: Action {}
struct CloseCameraAction: Action {}
struct FlashToggleAction: Action {}

My state now looks like this:

struct CameraState {
    var cameraIsVisible: Bool
    var flashOn: Bool
}
// not showing reducer changes as it self explanatory 
// what the state changes will be for my actions.

My View is where the complications start. If the user enabled flash and I am responding to a FlashToggleAction state change, how do I work the state change in my View?

func newState(state: CameraState?) {
        guard let state = state else {
            return
        }

        // this will get triggered regardless of whether
        // a change in the flash happened or not.
        self.toggleFlash(state.flashOn)

        // now the below lines will be executed even though 
        // the only change was a flash on action. 
        //I don't want my camera to opened again or closed.
        if state.cameraIsVisible {
            self.open()
        } else if !state.cameraIsVisible {
            self.close()
        }
    }

How do I respond to changes now? The only way I can think of handling this is by storing a reference to the old state and comparing the difference myself.

sethu
  • 8,181
  • 7
  • 39
  • 65
  • 1
    I don’t have time for a full answer right now but first things first. The logic for open/closing the image picker should not live inside the view. The view controller that you pass as a weak car is most likely the right candidate for conformance to StoreSubscriber. For this action I typically store a didAskForCamera state that the trigger the opening of the camera on the newState callback. You then reset that variable to false again so it doesn’t get called multiple times for every state change. Finally create a separate state for camera view with actions like flashon, flashoff (not toggle). – Rog Aug 06 '17 at 19:17
  • 1
    Also for simple actions like the above I find enum cases work very nicely. ie enum CameraActions: ActionType { case flashOn, case flashOff, etc) – Rog Aug 06 '17 at 19:19

1 Answers1

1

In this case, my first question is really: do you need to handle this as part of your app state? Does someone ever need to be notified about the camera state change? If not, keep this as an implementation detail inside your UI layer. let the biew controller open the camera on its own, take a resulting image, then dispatch DoStuffWithImage(imageFromCam).

This is just a general advice: do not model your UIKit-specific interactions in ReSwift. Do model the data flow that's interesting. Then make the UI components work towards that goal.

Your real question hints at: How do I partition store subscribers?

Currently, you approach the problem using a single subscriber for the camera-related stuff. But you could just as well write 1 subscriber for each independently modifiable component. Like the flash toggle. Then you only have to check for state changes in regard to the flash toggle there, ignoring other settings; although in this case the flash toggle thing could make use of knowing if he camera is actually on -- or you reset the flash state when the camera closes to take care of that", effectively moving the "flash AND camera active" logic down to the reducer.

I can come up with more approaches and ideas, but ultimately it boils down to this: what is a component in your app? Is the camera state control a central piece of the app's state, or just a minute detail? Weighing this early on in the design process can help find a fitting solution.

ctietze
  • 2,805
  • 25
  • 46
  • Thanks ctietze. The Camera is a common component and any ViewController should be able to open the camera by triggering an Action. I like the philosophy of redux which says any changes in the View are all triggered via Actions. This gives us the predicability we desire. Thanks for the idea about the partitioning of subscribers. Will try that now. – sethu Aug 07 '17 at 04:10