Share
Embed
Revisiting Massive View Controller
2019/4/24に行われたLINE Developer meetup #53 in KYOTOでの登壇資料です
LINE Developers
April 24, 2019
Transcript
76 slides
-
MVC @ IOS SU PING-CHEN (PC) LINE Fukuoka LINE Developer
meetup #53 in KYOTO -
ABOUT ME ▸ ોฑਉɹεΡɹͼʔΜͪ͐Μ ▸ Taiwanese ▸ iOS developer (6
years) ▸ LINE Fukuoka iOS developer ▸ LINE͍ ▸ LINE app -
WE ARE DEVELOPERS ▸ A good developer can ▸ Notice
problems ▸ Find existing solutions -
WE ARE DEVELOPERS ▸ A good developer can ▸ Notice
problems ▸ Find existing solutions MVVM+React -
WE ARE DEVELOPERS ▸ A good developer can ▸ Notice
problems ▸ Find existing solutions MVVM+React MVP -
WE ARE DEVELOPERS ▸ A good developer can ▸ Notice
problems ▸ Find existing solutions MVVM+React MVP Flux -
WE ARE DEVELOPERS ▸ A good developer can ▸ Notice
problems ▸ Find existing solutions MVVM+React MVP Redux Flux -
WE ARE DEVELOPERS ▸ A good developer can ▸ Notice
problems ▸ Find existing solutions ▸ Implement the solution MVVM+React MVP Redux Flux -
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() } } -
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 -
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 -
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 -
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 -
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 -
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. -
TEXT MVC ON MACOS UIVIEWCONTROLLER / NSCONTROLLER UIVIEW MODEL user
action update update notify -
TEXT TRADITIONAL MVC UIVIEWCONTROLLER / NSCONTROLLER UIVIEW SUBCLASS MODEL user
action update update notify Get changed state -
UIVIEWCONTROLLER ▸ Presenting/dismissing ▸ View life cycle ▸ View transitioning
▸ Handle view events -
UIVIEW AN OBJECT THAT MANAGES THE CONTENT FOR A RECTANGULAR
AREA ON THE SCREEN. -
UIVIEW ▸ View hierarchy management ▸ Control event management ▸
Rendering attributes -
UIVIEW ▸ View hierarchy management ▸ Control event management ▸
Rendering attributes ▸ Layout management -
UIVIEW ▸ View hierarchy management ▸ Control event management ▸
Rendering attributes ▸ Layout management ▸ Animations -
SUBCLASSING UIVIEW ▸ Move UIView dependent code from view controller
▸ Self-managed UIView UIVIEWCONTROLLER UIVIEW SETUP CODE -
SUBCLASSING UIVIEW ▸ Move UIView dependent code from view controller
▸ Self-managed UIView UIVIEWCONTROLLER UIVIEW SETUP CODE -
SUBCLASSING UIVIEW ▸ Move UIView dependent code from view controller
▸ Self-managed UIView UIVIEWCONTROLLER UIVIEW SETUP CODE EASIER TO TEST -
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 } } } -
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) } } } } -
SUBCLASS UIVIEWCONTROLLER ▸ Extract your model/networking logic from VC ▸
Understand your data source (async/sync/both) UIVIEWCONTROLLER NETWORKING MODEL -
SUBCLASS UIVIEWCONTROLLER ▸ Extract your model/networking logic from VC ▸
Understand your data source (async/sync/both) UIVIEWCONTROLLER NETWORKING MODEL HARD TO UNIT TEST -
SUBCLASS UIVIEWCONTROLLER ▸ Extract your model/networking logic from VC ▸
Understand your data source (async/sync/both) UIVIEWCONTROLLER NETWORKING MODEL CONTROLLER CODE ONLY -
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) } } -
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) } } } -
TEXT COMPLEX VIEW CONTROLLER ▸ Use child VC to present
contents ▸ Define protocol to pass model -
TEXT COMPLEX VIEW CONTROLLER ▸ Use child VC to present
contents ▸ Define protocol to pass model -
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 } } -
TEXT COMPLEX VIEW CONTROLLER ▸ Design view controller hierarchy ▸
Present child VC dynamically ▸ Child VC has dynamic frame size -
TEXT COMPLEX VIEW CONTROLLER ▸ Design view controller hierarchy ▸
Present child VC dynamically ▸ Child VC has dynamic frame size -
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)! } } -
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() } } -
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) } } -
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!