2
\$\begingroup\$

im trying to migrate my very large swift view controller to mvvm but it still feels very large, can you guys give me any advice

What this controller does is simple it shows a UIView on which i can draw the buttons that I display are here to manage the lines that i have drawn on the canvas.

For example to delete the last line drawn, delete all lines, change the color etc ...

What I want to do is to create my buttons while conforming to MVVM, so its not as ugly as my view controller is right now

@objc class VideoEditorViewController: UIViewController {
    
    let canvas = Canvas()
    let picker = UIColorPickerViewController()
    
    
    func setCanvasUI() {
        self.view.addSubview(canvas)
        canvas.backgroundColor = .clear
        let undoButton = CanvasButtonsViewModel(frame: .zero)
        undoButton.configure(with: CanvasModelButtonModel(image:
                                                        UIImage(systemName: "arrowshape.turn.up.left.fill")?
                                                        .withTintColor(.white, renderingMode: .alwaysOriginal), width: 50, height: 50, backgroundColor: .white))
        undoButton.addTarget(self, action: #selector(handleUndo), for: .touchUpInside)
        let colorPicker = CanvasButtonsViewModel(frame: .zero)
        colorPicker.configure(with: CanvasModelButtonModel(image:
                                                        UIImage(systemName: "pencil.circle")?
                                                        .withTintColor(.white, renderingMode: .alwaysOriginal), width: 50, height: 50, backgroundColor: .white))
        colorPicker.addTarget(self, action: #selector(ColorPicker), for: .touchUpInside)
        let trashCanButton = CanvasButtonsViewModel(frame: .zero)
        trashCanButton.configure(with: CanvasModelButtonModel(image:
                                                        UIImage(systemName: "trash")?
                                                        .withTintColor(.white, renderingMode: .alwaysOriginal), width: 50, height: 50, backgroundColor: .white))
        trashCanButton.addTarget(self, action: #selector(handleClear), for: .touchUpInside)
        let uploadViewButton = CanvasButtonsViewModel(frame: .zero)
        uploadViewButton.configure(with: CanvasModelButtonModel(image:
                                                                    UIImage(systemName: "envelope")?
                                                                    .withTintColor(.white, renderingMode: .alwaysOriginal), width: 51, height: 48, backgroundColor: .white))
        let test = CanvasButtonsViewModel(frame: .zero)
        test.configure(with: CanvasModelButtonModel(image:
                                                                    UIImage(systemName: "pencil.circle")?
                                                                    .withTintColor(.white, renderingMode: .alwaysOriginal), width: 51, height: 48, backgroundColor: .white))
        test.addTarget(self, action: #selector(testdraw), for: .touchUpInside)
        let stackView = UIStackView(arrangedSubviews: [
            undoButton,
            trashCanButton,
            colorPicker,
            uploadViewButton,
            test
            
        ])
        view.addSubview(stackView)
        stackView.axis = .vertical
        stackView.bringSubviewToFront(self.view)
        stackView.spacing = 30
        stackView.snp.makeConstraints { (make) in
            make.right.equalTo(view.snp_rightMargin).offset(-20)
            make.top.equalTo(view.snp_topMargin)
        }
        canvas.frame = view.frame
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        setCanvasUI()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}


extension VideoEditorViewController: UIColorPickerViewControllerDelegate, UIImagePickerControllerDelegate {
    @objc func handleUndo() {
        canvas.undo()
    }
    @objc func handleClear() {
        canvas.undoAll()
    }
    @objc func ColorPicker() {
        present(picker, animated: true, completion: nil)
        canvas.setStrokeColor(color: picker.selectedColor)
    }
    @objc func testdraw() {
        if (canvas.isDrawable == false) {
            canvas.setDrawable(state: true)
            return
        }
        if (canvas.isDrawable == true) {
            canvas.setDrawable(state: false)
            return
        }
    }
}

Canvas View :

import Foundation
import UIKit
import SnapKit

class Canvas: UIView {
    
    private var strokeColor = UIColor.black
    private var strokeWidth: Float = 10
    public var isDrawable: Bool = false
    
    public func setStrokeColor(color: UIColor) {
        self.strokeColor = color
    }
    
    public func setDrawable(state: Bool) {
        self.isDrawable = state
    }
    
    public func undo() {
        _ = lines.popLast()
        setNeedsDisplay()
    }
    public func undoAll() {
        lines.removeAll()
        setNeedsDisplay()
    }
    
    public func changeWidth(width: Float) {
        self.strokeWidth = width
        setNeedsDisplay()
    }
    public func draw() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
    
        lines.forEach { (line) in
            context.setStrokeColor(line.color.cgColor)
            context.setLineWidth(CGFloat(line.strokeWidth))
            context.setLineCap(.round)
        for(i, p) in line.points.enumerated() {
                if (i == 0) {
                    context.move(to: p)
                } else {
                    context.addLine(to: p)
                }
            }
            context.strokePath()
        }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        if (isDrawable == true) {
            draw()
        }
    }
    var lines = [Line]()
    
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        lines.append(Line.init(strokeWidth: strokeWidth, color: strokeColor, points: [], drawable: isDrawable))
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if (isDrawable == true) {
            guard let point = touches.first?.location(in: nil) else { return }
        
            guard var lastLine = lines.popLast() else { return }
            lastLine.points.append(point)
            lines.append(lastLine)
            print(point)
            setNeedsDisplay()
        }
    }
}

Canvas Struct to draw lines :

struct Line {
    let strokeWidth: Float
    let color: UIColor
    var points: [CGPoint]
    var drawable: Bool
}

My view model

final class CanvasButtonsViewModel: UIButton {
    
    let image: UIImageView = {
        let image = UIImageView()
        image.image = UIImage()
        image.image?.withTintColor(.white, renderingMode: .alwaysOriginal)
        return image
    }()

    func setConfig(size: CGFloat) -> UIImage.SymbolConfiguration {
        let largeConfig = UIImage.SymbolConfiguration(pointSize: size, weight: .bold, scale: .large)
        return largeConfig
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func configure(with viewModel: CanvasModelButtonModel) {
        self.addSubview(image)
        image.image = viewModel.image
        image.frame = CGRect(x: 0, y: 0, width: viewModel.width, height: viewModel.height)
    }
    override func layoutSubviews() {
        super.layoutSubviews()
    }
}

The model

struct CanvasModelButtonModel {
    let image: UIImage?
    let width: CGFloat
    let height: CGFloat
    let backgroundColor: UIColor?
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Welcome to Code Review! What task does this code accomplish? Please tell us, and also make that the title of the question via edit. Maybe you missed the placeholder on the title element: "State the task that your code accomplishes. Make your title distinctive.". Also from How to Ask: "State what your code does in your title, not your main concerns about it.". \$\endgroup\$ Commented Nov 20, 2020 at 16:24

2 Answers 2

3
\$\begingroup\$

The major observation here is that CanvasButtonsViewModel is a UIButton subclass, which is, by definition, a “view”, not a “view model”. I would suggest renaming this to CanvasButton.

There are two salient aspects to a “view model” within MVVM that are missing here:

  1. A view model is generally an abstract type, not tied to any particular UI framework (not UIKit, not AppKit, etc.). The goal is to have an abstract type that encapsulates all of the model data used by a particular view (e.g., the sets of coordinate pairs, colors, and widths that constitute the abstract representation of the diagram).

    E.g., the view controller might therefore have a view model that captures the current diagram (as outlined above), but also capture the state-related variables (current color, the default line width for new strokes, etc.).

    Now, at this nascent stage of your project, this abstraction might offer modest benefit, but when you start adding facilities to saving and loading drawings from persistent storage, constructing smoothed renditions of paths, etc., this sort of abstraction starts to bear fruit, avoiding the cluttering of the view layer (the UIView and UIViewController) with all this ancillary stuff.

    This abstraction of the view model layer into a separate object is quite easy and natural, and makes it easier to reason about as the app grows in complexity.

    That having been said, you appear to be contemplating a view model for the button that consists of basic configuration information (the image, width, height, and the background color). Those are basic properties of any button, and really do not warrant a separate view model for the button itself, IMHO. You can, if you really want, but there is very little value in doing so. Personally, I would lose the “view model” of the button, and focus on the broader “view model” of the view (i.e, the lines, the default color for new lines, etc.).

  2. There is a second dimension to MVVM, beyond the mere “view model” abstraction. Namely, MVVM generally is understood to entail a “data binding” between the view and the model. The idea is that you hook it up once, and any changes in the model will be automatically reflected in the view and vice versa.

    This aspect of MVVM is a less natural fit in UIKit without resorting to third-party binding libraries. (It is, however, a very natural fit with SwiftUI with its ObservableObject types, Bindable linkages with UI controls, and in its newer Observation framework. But that is a separate topic altogether.)

    That having been said, while there are various ways to achieve this sort of binding, one technique in UIKit apps is to use closures which you specify during the construction of the view in the view controller:

    • You might have a closure that the view uses to inform the view model of user input (e.g., a gesture started, a gesture continued, etc.);
    • You might have closure in the view model to inform the view when it needs to be re-rendered because the model changed; and
    • You might have closures on your buttons to inform the view model that the user tapped on the button, triggering some update in the view model.

    Note, after doing this, we no longer need to set targets for button clicks or the like. We just configure the view (including all of these closures which are the glue for our MVVM), and you are done.

So it comes down to whether you are simply looking for the “view” vs “view model” abstraction, or whether you are aspiring for a complete MVVM implementation. But here is a closure-based rendition:

The view controller:

class VideoEditorViewController: UIViewController {
    private let viewModel = VideoEditorViewModel()
    private let canvas = CanvasView()
    private let picker = UIColorPickerViewController()

    override func viewDidLoad() {
        super.viewDidLoad()
        setCanvasUI()
    }
}

// MARK: - Private interface

private extension VideoEditorViewController {
    func setCanvasUI() {
        view.addSubview(canvas)
        canvas.backgroundColor = .clear

        bindViewModelToUpdateCanvas()
        bindCanvasToUpdateViewModel()
        addButtons()
    }

    func bindViewModelToUpdateCanvas() {
        viewModel.onUpdateDrawing = { [weak self] lines in
            self?.canvas.lines = lines
        }
    }

    func bindCanvasToUpdateViewModel() {
        canvas.onStartGesture = { [weak self] point in
            self?.viewModel.startNewLine(at: point)
        }

        canvas.onContinueGesture = { [weak self] points in
            self?.viewModel.appendToLastLine(points)
        }
    }

    func addButtons() {
        let size = CGSize(width: 50, height: 50)

        let undoButton = CanvasButton(size: size, image: UIImage(systemName: "arrowshape.turn.up.left.fill")) { [weak self] _ in
            self?.viewModel.undo()
        }

        let colorPicker = CanvasButton(size: size, image: UIImage(systemName: "paintpalette.fill")) { [weak self] _ in
            guard let self else { return }
            picker.delegate = self
            present(picker, animated: true)
        }

        let trashCanButton = CanvasButton(size: size, image: UIImage(systemName: "trash")) { [weak self] _ in
            self?.viewModel.undoAll()
        }

        let uploadViewButton = CanvasButton(size: size, image: UIImage(systemName: "envelope")) { _ in // [weak self]
            print("upload not implemented")
        }

        let toggleDrawing = CanvasButton(size: size, image: UIImage(systemName: "pencil.circle")) { [weak self] _ in
            self?.viewModel.isDrawable.toggle()
        }

        let stackView = UIStackView(arrangedSubviews: [
            undoButton,
            trashCanButton,
            colorPicker,
            uploadViewButton,
            toggleDrawing
        ])
        view.addSubview(stackView)

        stackView.axis = .vertical
        stackView.bringSubviewToFront(view)
        stackView.spacing = 30
        stackView.snp.makeConstraints { make in
            make.right.equalTo(view.snp_rightMargin).offset(-20)
            make.top.equalTo(view.snp_topMargin)
        }
        canvas.frame = view.bounds
    }
}

// MARK: - UIColorPickerViewControllerDelegate

extension VideoEditorViewController: UIColorPickerViewControllerDelegate {
    func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
        viewModel.strokeColor = picker.selectedColor
    }
}

Note, no @IBAction or @objc functions. Just hook all of the bindings with closures, as outlined above.

Its view model:

class VideoEditorViewModel {
    var strokeColor: UIColor = .blue
    var strokeWidth: Float = 10
    var isDrawable: Bool = true
    private(set) var lines: [Line] = [] { didSet { onUpdateDrawing?(lines) } }
    var onUpdateDrawing: (([Line]) -> Void)?
}

extension VideoEditorViewModel {
    @discardableResult
    func undo() -> Line? {
        lines.popLast()
    }

    func undoAll() {
        lines.removeAll()
    }

    func startNewLine(at point: CGPoint?) {
        let line = Line(
            strokeWidth: strokeWidth,
            color: strokeColor,
            points: [point].compactMap { $0 },
            drawable: isDrawable
        )
        lines.append(line)
    }

    func appendToLastLine(_ points: [CGPoint]?) {
        guard let points else { return }

        let index = lines.count - 1
        if index >= 0 {
            lines[index].points.append(contentsOf: points)
        }
    }
}

CanvasView:

class CanvasView: UIView {
    var lines: [Line] = [] { didSet { setNeedsDisplay() } }

    var onStartGesture: ((CGPoint?) -> Void)?
    var onContinueGesture: (([CGPoint]) -> Void)?

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        isUserInteractionEnabled = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        lines
            .filter { $0.drawable }
            .forEach { line in
                let path = line.simplePath
                path.lineJoinStyle = .round
                path.lineCapStyle = .round
                path.lineWidth = CGFloat(line.strokeWidth)

                line.color.setStroke()

                path.stroke()
            }
    }
}

// MARK: - Touches

extension CanvasView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        onStartGesture?(touches.first?.location(in: self))
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        if let touches = event?.coalescedTouches(for: touch) {
            onContinueGesture?(touches.compactMap { $0.location(in: self) })
        } else if let point = touches.first?.location(in: self) {
            onContinueGesture?([point])
        }
    }
}

Note the CanvasView (formerly just Canvas) just renders a view and captures user input.

Now, there are different ways to skin the cat, but this is an example of (a) using closures to bind view models and views; and (b) using view models to abstract implementation details from the views. There are lots of ways of achieving the same, but hopefully this illustrates the idea.

I also created a Github repo with the above. If you look at the commit history, you can see the evolution from your original code, to some cosmetic changes, to a few bug fixes, to a rendition where I created a view model for just the CanvasView, to one where I created a view model for the whole view controller.


There are some minor issues in your original code snippet, FYI:

  • You are constructing the stack view in viewDidAppear. Each time it appears, you are going to create a new stack view. This should be done only once, in viewDidLoad.

  • The color picker is presenting the picker and as soon as that presentation is complete, you are setting the canvas color. This should be moved into the UIColorPickerViewControllerDelegate method colorPickerViewController(_:didSelect:continuously:).

  • You are not using UIImagePickerControllerDelegate, so that conformance declaration (esp where you do not implement any of these methods) is unnecessary.

  • In touchesBegan, you really should capture where the gesture started. Right now, you are discarding the first point of the gesture.

  • Where you are extracting the position from Set<UITouch>, you really should specify the view as self, otherwise you will be getting coordinates in the window’s coordinate system, not your current view’s coordinate system. (You might not notice if the view is located at (0, 0), but if it was not, you would not get the coordinates you expected.)

  • Likewise, when setting the frame of canvas, you should use the coordinate system of view. Specifically, you should reference view.bounds, not view.frame; the latter is in the coordinate system of the superview of view. In this case, there is not an observable difference, but if this view was not at (0,0), you will start to manifest problems.

  • Where extracting updates in gestures, you should probably consider coalesced touches, for more fine-grained details on touch information (on capable hardware). It’s not relevant to the broader MVVM questions, but a consideration when processing touches.

  • Personally, I would also capture predictive touches to reduce lag in the UI (but you need a slightly more refined model to capture this dimension). See https://stackoverflow.com/a/64709364/1271826 or https://stackoverflow.com/a/58456179/1271826.

  • When rendering gestures, I would also be inclined to smooth them via Catmull Rom or Hermite splines. See https://stackoverflow.com/a/34583708/1271826. Again, unrelated to the broader MVVM question, so I will leave that to the reader.

  • I must confess that it offends my MVVM-purist tendencies that a model object (like Line) is still entangled with UIKit. This is because of the UIColor property. I might be inclined to abstract this to some non-platform-specific structure, but, again, that is beyond the scope of this question.

    But as a general design principle, many of us avoid entangling model objects with platform-specific types like this. FWIW.

  • I did not grok the purpose of the drawable property of Line. I attempted to preserve that in my example, but I am not sure what the utility of capturing a Line that was not drawable. I interpreted this as a path that was not to be rendered, but that doesn’t make sense IMHO, so I apologize if misunderstood your intent. But, nonetheless, I attempted to preserve the idea. It is not quite relevant to the broader question, but FWIW.

\$\endgroup\$
0
\$\begingroup\$
// General Comments
/**
 - All view model should not be inherited from UIView nor UIViewController they should be their own separate classes/structs
 - The provided code is all view and UI logic which do not need view models
 */
import Foundation
import UIKit
import SnapKit

@objc
class VideoEditorViewController: UIViewController {
    let canvas = Canvas()
    let picker = UIColorPickerViewController()
    
    // Life cycle functions should be at the top of the view controller after the variables and outlets
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.setCanvasUI()
    }
    // no need to have this function as it only calls its parent function
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func setCanvasUI() {
        self.view.addSubview(self.canvas)
        self.canvas.backgroundColor = .clear
        // You should seperate between each UIView with at least one space for readability
        let undoButton = CanvasButton(frame: .zero)
        let undoButtonModel = CanvasButtonModel(image: UIImage(systemName: "arrowshape.turn.up.left.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal), width: 50, height: 50, backgroundColor: .white)
        undoButton.configure(with: undoButtonModel)
        undoButton.addTarget(self, action: #selector(handleUndo), for: .touchUpInside)
        
        let colorPicker = CanvasButton(frame: .zero)
        let colorPickerModle = CanvasButtonModel(image:UIImage(systemName: "pencil.circle")?.withTintColor(.white, renderingMode: .alwaysOriginal), width: 50, height: 50, backgroundColor: .white)
        colorPicker.configure(with: colorPickerModle)
        colorPicker.addTarget(self, action: #selector(ColorPicker), for: .touchUpInside)
        
        let trashCanButton = CanvasButton(frame: .zero)
        let trashCanModel = CanvasButtonModel(image: UIImage(systemName: "trash")?.withTintColor(.white, renderingMode: .alwaysOriginal), width: 50, height: 50, backgroundColor: .white)
        trashCanButton.configure(with: trashCanModel)
        trashCanButton.addTarget(self, action: #selector(handleClear), for: .touchUpInside)
        
        let uploadViewButton = CanvasButton(frame: .zero)
        let uploadButtonModel = CanvasButtonModel(image:UIImage(systemName: "envelope")?.withTintColor(.white, renderingMode: .alwaysOriginal), width: 51, height: 48, backgroundColor: .white)
        uploadViewButton.configure(with: uploadButtonModel)
        
        let test = CanvasButton(frame: .zero)
        let testModel = CanvasButtonModel(image: UIImage(systemName: "pencil.circle")?.withTintColor(.white, renderingMode: .alwaysOriginal), width: 51, height: 48, backgroundColor: .white)
        test.configure(with: testModel)
        test.addTarget(self, action: #selector(testdraw), for: .touchUpInside)
        
        let stackView = UIStackView(arrangedSubviews: [
            undoButton,
            trashCanButton,
            colorPicker,
            uploadViewButton,
            test
        ])
        
        view.addSubview(stackView)
        
        stackView.axis = .vertical
        stackView.bringSubviewToFront(self.view)
        stackView.spacing = 30
        stackView.snp.makeConstraints { (make) in
            make.right.equalTo(view.snp_rightMargin).offset(-20)
            make.top.equalTo(view.snp_topMargin)
        }
        self.canvas.frame = view.frame
    }
}

// We should use this protocol to make sure than anyone who is using the canvas with these buttons should have these functions implemented. Although I would personally prefer to have all these buttons inside the Canvas UIView along with its functionalities
@objc
protocol CanvasButtonsDelegate {
    func handleUndo()
    func handleClear()
    func colorPicker()
    func testdraw()
}

extension VideoEditorViewController: UIColorPickerViewControllerDelegate, UIImagePickerControllerDelegate, CanvasButtonsDelegate {
    // There should be at least one space between each function declaration
    @objc func handleUndo() {
        self.canvas.undo()
    }
    
    @objc func handleClear() {
        self.canvas.undoAll()
    }
    // function name should start with lower case
    @objc func colorPicker() {
        present(self.picker, animated: true, completion: nil)
        self.canvas.setStrokeColor(color: self.picker.selectedColor)
    }
    
    @objc func testdraw() {
        if (self.canvas.isDrawable == false) {
            self.canvas.setDrawable(state: true)
            return
        }
        if (self.canvas.isDrawable == true) {
            self.canvas.setDrawable(state: false)
            return
        }
    }
}

class Canvas: UIView {
    private var strokeColor = UIColor.black
    private var strokeWidth: Float = 10
    public var isDrawable: Bool = false
    var lines = [Line]()
    
    public func setStrokeColor(color: UIColor) {
        self.strokeColor = color
    }
    
    public func setDrawable(state: Bool) {
        self.isDrawable = state
    }
    
    public func undo() {
        _ = self.lines.popLast()
        self.setNeedsDisplay()
    }
    public func undoAll() {
        self.lines.removeAll()
        self.setNeedsDisplay()
    }
    
    public func changeWidth(width: Float) {
        self.strokeWidth = width
        self.setNeedsDisplay()
    }
    
    public func draw() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
    
        self.lines.forEach { (line) in
            context.setStrokeColor(line.color.cgColor)
            context.setLineWidth(CGFloat(line.strokeWidth))
            context.setLineCap(.round)
            for(i, p) in line.points.enumerated() {
                if (i == 0) {
                    context.move(to: p)
                } else {
                    context.addLine(to: p)
                }
            }
            context.strokePath()
        }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        if (self.isDrawable == true) {
            self.draw()
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.lines.append(Line(strokeWidth: self.strokeWidth, color: self.strokeColor, points: [], drawable: self.isDrawable))
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if (self.isDrawable == true) {
            guard let point = touches.first?.location(in: nil) else { return }
        
            guard var lastLine = self.lines.popLast() else { return }
            lastLine.points.append(point)
            self.lines.append(lastLine)
            print(point)
            self.setNeedsDisplay()
        }
    }
}

final class CanvasButton: UIButton {
    let image: UIImageView = {
        let image = UIImageView()
        image.image = UIImage()
        image.image?.withTintColor(.white, renderingMode: .alwaysOriginal)
        return image
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setConfig(size: CGFloat) -> UIImage.SymbolConfiguration {
        let largeConfig = UIImage.SymbolConfiguration(pointSize: size, weight: .bold, scale: .large)
        return largeConfig
    }
    
    func configure(with viewModel: CanvasButtonModel) {
        self.addSubview(self.image)
        self.image.image = viewModel.image
        self.image.frame = CGRect(x: 0, y: 0, width: viewModel.width, height: viewModel.height)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
    }
}

/**
 - CanvasButtonsViewModel should be CanvasButtonModel as it doesn't reflect anything related to view model concept
 - Also it should be Button not Buttons
 - CanvasModelButtonModel is just a model a container, not a view model
 - The naming convention is not being followed here ex: CanvasModelButtonModel. It should be CanvasButtonModel
*/
struct CanvasButtonModel {
    let image: UIImage?
    let width: CGFloat
    let height: CGFloat
    let backgroundColor: UIColor?
}

struct Line {
    let strokeWidth: Float
    let color: UIColor
    var points: [CGPoint]
    var drawable: Bool
}

I'm failing to understand the reason behind writing the code in such a way so I will just provide a very simple example of MVVM pattern in iOS swift using UIKit

import Foundation
import UIKit

// just a temp way to pass errors to the UI
enum ViewModelError: Error {
    case loginFailed
}

// the list of action any UI is using the provided view model should handle
protocol ViewModelDelegate {
    func onLoginSuccess(response: NSObject)
    func onLoginFailuar(error: Error)
}

// so the view will only be able to access the available functionality of this view model and not the whole properties and functionalities
// also for easier testing
@objc
protocol ViewModelInterface {
    func login()
}

// will contain all the business and API calling logic like local validation, on network error, etc
final class ViewModel: ViewModelInterface {
    
    var delegate: ViewModelDelegate?
    
    init(delegate: ViewModelDelegate? = nil) {
        self.delegate = delegate
    }
    
    @objc func login() {
        // async call here to login the user
        if true { // success
            self.delegate?.onLoginSuccess(response: NSObject())
        } else { // failed
            self.delegate?.onLoginFailuar(error: ViewModelError.loginFailed)
        }
    }
}

// will contain all the UI logic
class ViewController: UIViewController {
    // MARK:- Outlets
    let loginButton: UIButton = UIButton()
    
    // MARK:- ViewModel
    var vm: ViewModelInterface!
    
    // MARK:- Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // set the view model delegate
        self.vm = ViewModel(delegate: self)
        
        // UI is dump and should not know what to do here and it is the view model responsibility to know what to do on any kind of events
        self.loginButton.addTarget(self.vm, action: #selector(self.vm.login), for: .touchDown)
    }
}

extension ViewController: ViewModelDelegate {
    func onLoginSuccess(response: NSObject) {
        // 1. save token in the user default
        // 2. navigate to home screen
    }
    
    func onLoginFailuar(error: Error) {
        // show error on the UI
    }
}

```
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.