Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Revisiting Massive View Controller

Revisiting Massive View Controller

2019/4/24に行われたLINE Developer meetup #53 in KYOTOでの登壇資料です

LINE Developers

April 24, 2019
Tweet

More Decks by LINE Developers

Other Decks in Technology

Transcript

  1. ABOUT ME ▸ ોฑਉɹεΡɹͼʔΜͪ͐Μ ▸ Taiwanese ▸ iOS developer (6

    years) ▸ LINE Fukuoka iOS developer ▸ LINE઎͍ ▸ LINE app
  2. WE ARE DEVELOPERS ▸ A good developer can ▸ Notice

    problems ▸ Find existing solutions
  3. WE ARE DEVELOPERS ▸ A good developer can ▸ Notice

    problems ▸ Find existing solutions MVVM+React
  4. WE ARE DEVELOPERS ▸ A good developer can ▸ Notice

    problems ▸ Find existing solutions MVVM+React MVP
  5. WE ARE DEVELOPERS ▸ A good developer can ▸ Notice

    problems ▸ Find existing solutions MVVM+React MVP Flux
  6. WE ARE DEVELOPERS ▸ A good developer can ▸ Notice

    problems ▸ Find existing solutions MVVM+React MVP Redux Flux
  7. WE ARE DEVELOPERS ▸ A good developer can ▸ Notice

    problems ▸ Find existing solutions ▸ Implement the solution MVVM+React MVP Redux Flux
  8. TEXT EXAMPLE - PLAIN VIEW CONTROLLER class ViewController: UIViewController {

    @IBOutlet var avatarImageContainerView: UIView! @IBOutlet var avatarImageView: UIImageView! private var avatarDataTask: URLSessionTask? private var imageUrl: URL? override func viewDidLoad() { super.viewDidLoad() avatarImageView.clipsToBounds = true // Set shadow and border to avatarImageContainerView } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() avatarImageContainerView.layer.cornerRadius = min(avatarImageContainerView.frame.width, avatarImageContainerView.frame.height) avatarImageView.layer.cornerRadius = min(avatarImageView.frame.width, avatarImageView.frame.height) } private func setAvatarImageUrl(_ url: URL?) { avatarDataTask?.cancel() imageUrl = url avatarImageView.image = nil guard imageUrl != nil else { return } avatarDataTask = URLSession.shared .dataTask(with: imageUrl!) { [weak self] (data, res, err) in guard let data = data else { return } self?.avatarImageView.image = UIImage(data: data) } avatarDataTask?.resume() } }
  9. Although it is possible to have views directly observe models

    to detect changes in state, it is best not to do so. A view object should always go through a mediating controller object to learn about changes in an model object. The reason is two-fold: developer.apple.com TEXT
  10. Although it is possible to have views directly observe models

    to detect changes in state, it is best not to do so. A view object should always go through a mediating controller object to learn about changes in an model object. The reason is two-fold: • If you use the bindings mechanism to have view objects directly observe the properties of model objects, you bypass all the advantages that NSController and its subclasses give your application: selection and placeholder management as well as the ability to commit and discard changes. developer.apple.com TEXT
  11. Although it is possible to have views directly observe models

    to detect changes in state, it is best not to do so. A view object should always go through a mediating controller object to learn about changes in an model object. The reason is two-fold: • If you use the bindings mechanism to have view objects directly observe the properties of model objects, you bypass all the advantages that NSController and its subclasses give your application: selection and placeholder management as well as the ability to commit and discard changes. • If you don't use the bindings mechanism, you have to subclass an existing view class to add the ability to observe change notifications posted by a model object. developer.apple.com TEXT
  12. Although it is possible to have views directly observe models

    to detect changes in state, it is best not to do so. A view object should always go through a mediating controller object to learn about changes in an model object. The reason is two-fold: • If you use the bindings mechanism to have view objects directly observe the properties of model objects, you bypass all the advantages that NSController and its subclasses give your application: selection and placeholder management as well as the ability to commit and discard changes. • If you don't use the bindings mechanism, you have to subclass an existing view class to add the ability to observe change notifications posted by a model object. developer.apple.com TEXT Use NSController(inside AppKit)/ViewController
  13. Although it is possible to have views directly observe models

    to detect changes in state, it is best not to do so. A view object should always go through a mediating controller object to learn about changes in an model object. The reason is two-fold: • If you use the bindings mechanism to have view objects directly observe the properties of model objects, you bypass all the advantages that NSController and its subclasses give your application: selection and placeholder management as well as the ability to commit and discard changes. • If you don't use the bindings mechanism, you have to subclass an existing view class to add the ability to observe change notifications posted by a model object. developer.apple.com TEXT Use NSController(inside AppKit)/ViewController IT IS SO GOOD
  14. Although it is possible to have views directly observe models

    to detect changes in state, it is best not to do so. A view object should always go through a mediating controller object to learn about changes in an model object. The reason is two-fold: • If you use the bindings mechanism to have view objects directly observe the properties of model objects, you bypass all the advantages that NSController and its subclasses give your application: selection and placeholder management as well as the ability to commit and discard changes. • If you don't use the bindings mechanism, you have to subclass an existing view class to add the ability to observe change notifications posted by a model object. developer.apple.com TEXT Use NSController(inside AppKit)/ViewController IT IS SO GOOD Fine, or you can subclass yourself.
  15. UIVIEW ▸ View hierarchy management ▸ Control event management ▸

    Rendering attributes ▸ Layout management ▸ Animations
  16. SUBCLASSING UIVIEW ▸ Move UIView dependent code from view controller

    ▸ Self-managed UIView UIVIEWCONTROLLER UIVIEW SETUP CODE
  17. SUBCLASSING UIVIEW ▸ Move UIView dependent code from view controller

    ▸ Self-managed UIView UIVIEWCONTROLLER UIVIEW SETUP CODE
  18. SUBCLASSING UIVIEW ▸ Move UIView dependent code from view controller

    ▸ Self-managed UIView UIVIEWCONTROLLER UIVIEW SETUP CODE EASIER TO TEST
  19. EXAMPLE - PLAIN VIEW CONTROLLER class AvatarIconButton: UIButton { var

    userInfo: UserInfo! { didSet { UserRepository.getUserAvatar(userInfo) { [weak self] result in if case .success(let image) = result { self?.setImage(image, for: .normal) } } } } override func awakeFromNib() { super.awakeFromNib() // setup view settings } override func layoutSubviews() { super.layoutSubviews() layer.cornerRadius = min(frame.width, frame.height) / 2 if let imageView = imageView { imageView.layer.cornerRadius = min(imageView.frame.width, imageView.frame.height) / 2 } } }
  20. EXAMPLE - PLAIN VIEW CONTROLLER class ViewController: UIViewController { @IBOutlet

    weak var avatarIconView: AvatarIconView! var userRepo: UserRepository = UserRepository() override func viewDidLoad() { super.viewDidLoad() userRepo.getUserInfo { [weak self] result in switch result { case .success(let userInfo): self?.avatarIconView.userInfo = userInfo case .failure(let err): print(err) } } } }
  21. SUBCLASS UIVIEWCONTROLLER ▸ Extract your model/networking logic from VC ▸

    Understand your data source (async/sync/both) UIVIEWCONTROLLER NETWORKING MODEL
  22. SUBCLASS UIVIEWCONTROLLER ▸ Extract your model/networking logic from VC ▸

    Understand your data source (async/sync/both) UIVIEWCONTROLLER NETWORKING MODEL HARD TO UNIT TEST
  23. SUBCLASS UIVIEWCONTROLLER ▸ Extract your model/networking logic from VC ▸

    Understand your data source (async/sync/both) UIVIEWCONTROLLER NETWORKING MODEL CONTROLLER CODE ONLY
  24. TEXT EXAMPLE - TABLE VIEW CONTROLLER class DataStore { private

    var titles = [String]() var count: Int { return titles.count } func data(from: IndexPath) -> String { return titles[from.row] } func move(from: IndexPath, to: IndexPath) { let compareResult = from.compare(to) guard compareResult != .orderedSame else { return } let item = titles.remove(at: from.row) titles.insert(item, at: (compareResult == .orderedAscending ? to.row - 1 : to.row)) } func remove(at: IndexPath) { titles.remove(at: at.row) } func createItem(at: IndexPath) { titles.insert("New Title", at: at.row) } }
  25. TEXT EXAMPLE - TABLE VIEW CONTROLLER class SampleViewController: UITableViewController {

    let dataStore: DataStore init(_ dataStore: DataStore) { self.dataStore = dataStore super.init(nibName: nil, bundle: nil) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) cell.textLabel?.text = dataStore.data(from: indexPath) return cell } override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { dataStore.move(from: sourceIndexPath, to: destinationIndexPath) } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { dataStore.remove(at: indexPath) } else if editingStyle == .insert { dataStore.createItem(at: indexPath) } } }
  26. TEXT COMPLEX VIEW CONTROLLER ▸ Use child VC to present

    contents ▸ Define protocol to pass model
  27. TEXT COMPLEX VIEW CONTROLLER ▸ Use child VC to present

    contents ▸ Define protocol to pass model
  28. TEXT EXAMPLE - CHILD VIEW CONTROLLER class ChildViewController: UIViewController {

    private var modelStore: MyModelStore! override func didMove(toParent parent: UIViewController?) { var dataProviderVC = parent while dataProviderVC != nil { if let dataProviderVC = dataProviderVC as? MyDataProviderProtocol { modelStore = dataProviderVC.getModelStore() break } dataProviderVC = dataProviderVC?.parent } // Unable to find data provider } }
  29. TEXT COMPLEX VIEW CONTROLLER ▸ Design view controller hierarchy ▸

    Present child VC dynamically ▸ Child VC has dynamic frame size
  30. TEXT COMPLEX VIEW CONTROLLER ▸ Design view controller hierarchy ▸

    Present child VC dynamically ▸ Child VC has dynamic frame size
  31. TEXT EXAMPLE - EMBEDDED VIEW CONTROLLER IN CELL private class

    DataStore { private var viewControllers = [IndexPath: UIViewController]() func generateViewController(at: IndexPath) -> UIViewController { let viewController = ChildViewController() viewControllers[at] = viewController return viewController } func removeViewController(at: IndexPath) -> UIViewController { return viewControllers.removeValue(forKey: at)! } }
  32. TEXT EXAMPLE - EMBEDDED VIEW CONTROLLER IN CELL class ParentViewController:

    UITableViewController { fileprivate let dataStore = DataStore() override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { let viewController = dataStore.generateViewController(at: indexPath) addChild(viewController) viewController.didMove(toParent: self) cell.contentView.addSubview(viewController.view) } override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { let viewController = dataStore.removeViewController(at: indexPath) viewController.view.removeFromSuperview() viewController.willMove(toParent: nil) viewController.removeFromParent() } }
  33. TEXT EXAMPLE - EMBEDDED VIEW CONTROLLER IN CELL class ParentTableViewCell:

    UITableViewCell { func appendCustomView(_ view: UIView) { // setup auto layout constraints view.frame = contentView.bounds contentView.addSubview(view) } }
  34. TEXT SUMMARY ▸ MVC can be clean and naive ▸

    You can create general and reusable UI ▸ There is no best design strategy for every app, select what meets your need ▸ MVVM(+React), Flux, Redux, … are still pretty awesome!