Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

of

MVVM with SwiftUI and Combine Slide 1 MVVM with SwiftUI and Combine Slide 2 MVVM with SwiftUI and Combine Slide 3 MVVM with SwiftUI and Combine Slide 4 MVVM with SwiftUI and Combine Slide 5 MVVM with SwiftUI and Combine Slide 6 MVVM with SwiftUI and Combine Slide 7 MVVM with SwiftUI and Combine Slide 8 MVVM with SwiftUI and Combine Slide 9 MVVM with SwiftUI and Combine Slide 10 MVVM with SwiftUI and Combine Slide 11 MVVM with SwiftUI and Combine Slide 12 MVVM with SwiftUI and Combine Slide 13 MVVM with SwiftUI and Combine Slide 14 MVVM with SwiftUI and Combine Slide 15 MVVM with SwiftUI and Combine Slide 16 MVVM with SwiftUI and Combine Slide 17 MVVM with SwiftUI and Combine Slide 18 MVVM with SwiftUI and Combine Slide 19 MVVM with SwiftUI and Combine Slide 20 MVVM with SwiftUI and Combine Slide 21 MVVM with SwiftUI and Combine Slide 22 MVVM with SwiftUI and Combine Slide 23 MVVM with SwiftUI and Combine Slide 24 MVVM with SwiftUI and Combine Slide 25 MVVM with SwiftUI and Combine Slide 26 MVVM with SwiftUI and Combine Slide 27 MVVM with SwiftUI and Combine Slide 28 MVVM with SwiftUI and Combine Slide 29 MVVM with SwiftUI and Combine Slide 30 MVVM with SwiftUI and Combine Slide 31 MVVM with SwiftUI and Combine Slide 32 MVVM with SwiftUI and Combine Slide 33 MVVM with SwiftUI and Combine Slide 34 MVVM with SwiftUI and Combine Slide 35 MVVM with SwiftUI and Combine Slide 36 MVVM with SwiftUI and Combine Slide 37 MVVM with SwiftUI and Combine Slide 38 MVVM with SwiftUI and Combine Slide 39 MVVM with SwiftUI and Combine Slide 40 MVVM with SwiftUI and Combine Slide 41 MVVM with SwiftUI and Combine Slide 42 MVVM with SwiftUI and Combine Slide 43 MVVM with SwiftUI and Combine Slide 44 MVVM with SwiftUI and Combine Slide 45 MVVM with SwiftUI and Combine Slide 46 MVVM with SwiftUI and Combine Slide 47 MVVM with SwiftUI and Combine Slide 48 MVVM with SwiftUI and Combine Slide 49 MVVM with SwiftUI and Combine Slide 50 MVVM with SwiftUI and Combine Slide 51 MVVM with SwiftUI and Combine Slide 52 MVVM with SwiftUI and Combine Slide 53 MVVM with SwiftUI and Combine Slide 54 MVVM with SwiftUI and Combine Slide 55 MVVM with SwiftUI and Combine Slide 56 MVVM with SwiftUI and Combine Slide 57 MVVM with SwiftUI and Combine Slide 58 MVVM with SwiftUI and Combine Slide 59 MVVM with SwiftUI and Combine Slide 60 MVVM with SwiftUI and Combine Slide 61 MVVM with SwiftUI and Combine Slide 62 MVVM with SwiftUI and Combine Slide 63 MVVM with SwiftUI and Combine Slide 64 MVVM with SwiftUI and Combine Slide 65 MVVM with SwiftUI and Combine Slide 66 MVVM with SwiftUI and Combine Slide 67 MVVM with SwiftUI and Combine Slide 68
Upcoming SlideShare
What to Upload to SlideShare
Next
Download to read offline and view in fullscreen.

0 Likes

Share

Download to read offline

MVVM with SwiftUI and Combine

Download to read offline

A brief wrap up and introduction to SwiftUI and Combine brought by Apple in WWDC 2019, as well as utilizing both to propose a View-Model-ViewModel (MVVM) structure.

Related Books

Free with a 30 day trial from Scribd

See all
  • Be the first to like this

MVVM with SwiftUI and Combine

  1. 1. MVVM with SwiftUI and Combine Tai-Lun Tseng 2019.11.15, Apple Taiwan
  2. 2. Agenda • SwiftUI • Combine • MVVM
  3. 3. SwiftUI import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } }
  4. 4. SwiftUI • Declarative • Merges code and visual design, instead of separation (like storyboard) • Prevents complex UIViewController codes
  5. 5. Traditional Wayimport UIKit class ChecklistCell: UITableViewCell { @IBOutlet var doneSwitch: UISwitch! @IBOutlet var titleLabel: UILabel! @IBOutlet var createdAtLabel: UILabel! func configure(for item: CheckItem) { self.titleLabel.text = item.title self.createdAtLabel.text = item.createdAt self.doneSwitch.isOn = item.done } } class ChecklistTableViewController : UIViewController, UITableViewDataSource { private var checklist = sampleChecklist func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { checklist.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "checklist_cell", for: indexPath) if let checklistCell = cell as? ChecklistCell { checklistCell.configure(for: checklist[indexPath.row]) } return cell } // ... } import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } }
  6. 6. Traditional Wayimport UIKit class ChecklistCell: UITableViewCell { @IBOutlet var doneSwitch: UISwitch! @IBOutlet var titleLabel: UILabel! @IBOutlet var createdAtLabel: UILabel! func configure(for item: CheckItem) { self.titleLabel.text = item.title self.createdAtLabel.text = item.createdAt self.doneSwitch.isOn = item.done } } class ChecklistTableViewController : UIViewController, UITableViewDataSource { private var checklist = sampleChecklist func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { checklist.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "checklist_cell", for: indexPath) if let checklistCell = cell as? ChecklistCell { checklistCell.configure(for: checklist[indexPath.row]) } return cell } // ... } import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } • 40+ lines of code • Requires Storyboard setup and linking • Adjust layout in both codes and Storyboard/Nibs • Supports all iOS versions
  7. 7. Traditional Wayimport UIKit class ChecklistCell: UITableViewCell { @IBOutlet var doneSwitch: UISwitch! @IBOutlet var titleLabel: UILabel! @IBOutlet var createdAtLabel: UILabel! func configure(for item: CheckItem) { self.titleLabel.text = item.title self.createdAtLabel.text = item.createdAt self.doneSwitch.isOn = item.done } } class ChecklistTableViewController : UIViewController, UITableViewDataSource { private var checklist = sampleChecklist func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { checklist.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "checklist_cell", for: indexPath) if let checklistCell = cell as? ChecklistCell { checklistCell.configure(for: checklist[indexPath.row]) } return cell } // ... } import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } • 15 lines of code • No Nib or Storyboard • Design layout in code directly, with the support of Canvas • Supports iOS 13+
  8. 8. New Syntax? import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } }
  9. 9. import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } Property Wrapper • "Wraps" original property with power-ups • Work on class/struct properties
  10. 10. import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } Property Wrapper Type: [CheckItem] Type: Binding<[CheckItem]>
  11. 11. import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } Opaque Type • Reversed generics • See associatedtype and typealias https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html
  12. 12. import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } Function Builder What is the return value of the closure?
  13. 13. import SwiftUI struct ChecklistView: View { @Binding var checklist: [CheckItem] var body: some View { List(checklist.indices) { index in Toggle(isOn: self.$checklist[index].done) { VStack(alignment: .leading) { Text(self.checklist[index].title) .bold() Text(self.checklist[index].createdAt) .foregroundColor(.gray) } } } } } Function Builder VStack(alignment: .leading) { let view1 = Text(self.checklist[index].title) .bold() let view2 = Text(self.checklist[index].createdAt) .foregroundColor(.gray) return ContentBuilder.buildBlock(view1, view2) }
  14. 14. Function Builder public struct VStack<Content> : View where Content : View { @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) // ... } https://developer.apple.com/documentation/swiftui/viewbuilder
  15. 15. SwiftUI Canvas
  16. 16. SwiftUI Canvas
  17. 17. SwiftUI Canvas
  18. 18. Styling struct ContentView: View { @State var text = "Hello World!" var body: some View { VStack(alignment: .trailing, spacing: nil) { TextField("Enter text", text: $text) .border(Color.black) .multilineTextAlignment(.trailing) .padding() Text(text.uppercased()) .foregroundColor(.white) .bold() .padding() }.background(Rectangle().foregroundColor(.blue)) } }
  19. 19. Styling struct ContentView: View { @State var text = "Hello World!" var body: some View { VStack(alignment: .trailing, spacing: nil) { TextField("Enter text", text: $text) .border(Color.black) .multilineTextAlignment(.trailing) .padding() Text(text.uppercased()) .foregroundColor(.white) .bold() .padding() }.background(Rectangle().foregroundColor(.blue)) } }
  20. 20. @State struct ContentView: View { @State var text = "Hello World!" var body: some View { VStack(alignment: .trailing, spacing: nil) { TextField("Enter text", text: $text) .border(Color.black) .multilineTextAlignment(.trailing) .padding() Text(text.uppercased()) .foregroundColor(.white) .bold() .padding() }.background(Rectangle().foregroundColor(.blue)) } } • When state is updated, view is invalidated automatically • @State values are managed by the view
  21. 21. class SearchViewModel: ObservableObject { @Published var searchResult: [SearchResultItem] = [] @Published var searchText: String = "" } ObservableObject • Present a single state by combining multiple state values • Use @Published instead of @State
  22. 22. class SearchViewModel: ObservableObject { @Published var searchResult: [SearchResultItem] = [] @Published var searchText: String = "" } struct ContentView: View { @ObservedObject var model = SearchViewModel() } ObservableObject and @ObservedObject
  23. 23. Single Source of Truth? struct BadgeView: View { @State var unreadCount = 0 // ... } struct UnreadListView: View { @State var unreadList: [String] = [] // ... } struct SocialMediaView: View { var body: some View { VStack { BadgeView() UnreadListView() } } } SocialMediaView BadgeView UnreadListView unreadCount unreadList
  24. 24. Single Source of Truth struct BadgeView: View { var unreadCount: Int // ... } struct UnreadListView: View { @Binding var unreadList: [String] // ... } struct SocialMediaView: View { @State var unreadList: [String] = [] var body: some View { VStack { BadgeView(unreadCount: unreadList.count) UnreadListView(unreadList: $unreadList) } } } SocialMediaView BadgeView UnreadListView unreadList.count unreadList unreadList • Use @Binding to pass down states
  25. 25. View State and ObservedObject @State ObservableObject View View View View • Use @Binding to pass down states • Use @ObservedObject instead of @State @ObservedObject
  26. 26. View EnvironmentObject ObservableObject View View View View .environmentObject() • Use @EnvironmentObject instead of @State • Indirectly pass values for more flexibility @EnvironmentObject @EnvironmentObject
  27. 27. Add SwiftUI to UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Create the SwiftUI view that provides the window contents. let contentView = ContentView() // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } } // ... }
  28. 28. In Playground... let contentView = ContentView() let host = UIHostingController(rootView: contentView) host.preferredContentSize = CGSize(width: 320, height: 480) // Present the view controller in the Live View window PlaygroundPage.current.liveView = host
  29. 29. Preview and Test Data
  30. 30. Preview and Test Data Design and write component here
  31. 31. Preview and Test Data Provide test data to the preview component
  32. 32. Combine • Process asynchronous events easily • Swift's official reactive programming library • 3rd libraries: • ReactiveCocoa • RxSwift
  33. 33. Basic Concepts • Publisher • Subscriber • Transformations
  34. 34. Publisher: Data Source • Publishers create a series of data over time • Think as an event stream 3 4 20 6 0-32 Type: Int time
  35. 35. Publisher Examples Just<Int>(1) 1 • Creates an event stream with only 1 value, and then finishes immediately
  36. 36. Timer.publish(every: 1, on: .main, in: .common) 14:20: 36 14:20: 37 14:20: 38 14:20: 39 14:20: 40 Publisher Examples • Creates an event stream that emits a Date object every second
  37. 37. NotificationCenter.default.publisher(for: NSControl.textDidChangeNotification, object: textField) HelloH He Hel Hell Publisher Examples • Listens to text changes on a NSTextField with Notification Center • Whenever text changes, it emits an event whose value is the NSTextField object
  38. 38. Subscriber: event listener struct TimerView : View { @ObservedObject var timerState: TimerState var body: some View { Text(timerState.timeText) } } Timer .publish(every: 1, on: .main, in: .common) .autoconnect() .sink { date in timerState.timeText = df.string(from: date) } Timer.publish(every: 1, on: .main, in: .common) 14:20: 36 14:20: 37 14:20: 38 14:20: 39 14:20: 40
  39. 39. Transformations NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: textField) .map { ($0 as! NSTextField).stringValue } .filter { $0.count > 2 } HelloH He Hel Hell "Hello""" "H" "He" "Hel" "Hell" "Hello""Hel" "Hell" map filter
  40. 40. Showcase: Search • Requirements • Send network request after user stopped key in for 1 second • Don't send request for same search texts
  41. 41. class SearchViewModel: ObservableObject { @Published var searchResult: [SearchResultItem] = [] @Published var searchText: String = "" init(searchRepository: SearchRepository) { $searchText .dropFirst(1) // ... .sink { result in self.searchResult = result } } } @Published as Publisher
  42. 42. Transformations $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable)
  43. 43. dropFirst $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable)
  44. 44. dropFirst $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) "G "Gu" "Gun" "Gund" "Gunda" "Gundam" "" "G "Gu" "Gun" "Gund" "Gunda" "Gundam" dropFirst(1)
  45. 45. debounce $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable)
  46. 46. debounce $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) "Gun" "Gundam" "G" "Gu" "Gun" "Gund" "Gunda" "Gundam" debounce(for: 1, scheduler: RunLoop.main)
  47. 47. removeDuplicates & filter $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable)
  48. 48. $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) removeDuplicates & filter "G "Gun" "Gun" "" removeDuplicates() "G "Gun" "" "G "Gun" filter { $0.count > 0 }
  49. 49. $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) URLSession.DataTaskPublisher
  50. 50. URLSession.DataTaskPublisher URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https://en.wikipedia.org/w/api.php?action=opensearch&search= (searchText)&limit=(self.limit)&namespace=0&format=json")!), session: .shared) (Data, Response) .map { $0.data } <5b, 22, 4b, 61, ...>
  51. 51. $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) flatMap
  52. 52. flatMap "Gun" "Gundam" <5b, 22, 4b, 61, ...> [SearchResultItem] <5b, 22, 4b, 61, ...> [SearchResultItem] compactMap compactMap URLSession.DataTaskPublisher URLSession.DataTaskPublisher
  53. 53. flatMap "Gun" "Gundam" [SearchResultItem] [SearchResultItem] .flatMap { searchText in URLSession.DataTaskPublisher(... }
  54. 54. $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) compactMap
  55. 55. Optional([SearchResultItem]) compactMap <5b, 22, 4b, 61, ...> <00, 00, 00, ...> Optional([SearchResultItem]) nil .map { self.parseSearchResult(data: $0) } [SearchResultItem] .filter( $0 != nil ) .map { $0! }
  56. 56. compactMap <5b, 22, 4b, 61, ...> <00, 00, 00, ...> [SearchResultItem] .compactMap { self.parseSearchResult(data: $0) }
  57. 57. $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) sink
  58. 58. $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 0 } .compactMap { $0.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) } .flatMap { searchText in URLSession.DataTaskPublisher(request: URLRequest(url: URL(string: "https:// en.wikipedia.org/w/api.php?action=opensearch&search=(searchText)&limit= (self.limit)&namespace=0&format=json")!), session: .shared) .map { $0.data } .catch { err -> Just<Data?> in print(err) return Just(nil) } .compactMap { $0 } } .compactMap { self.parseSearchResult(data: $0) } .receive(on: RunLoop.main) .sink { result in self.searchResult = result } .store(in: &cancellable) sink [SearchResultItem]
  59. 59. store • .sink() returns a subscription which conforms to Cancellable • Call cancellable.cancel() to cancel the subscription • Use .store() to manage subscriptions let cancellable = $searchText .dropFirst(1) ... .sink { result in self.searchResult = result } cancellable.store(in: &cancellableSet) $searchText .dropFirst(1) ... .sink { result in self.searchResult = result } .store(in: &cancellableSet)
  60. 60. Model-View-ViewModel (MVVM) • Variation of model-view-presenter (MVP) • More concise codes and data flow • View knows existence of ViewModel, but not vise-versa • ViewModel sends data to View via subscription • Same as ViewModel and Model • Non-UI logics and data layers sit in Models
  61. 61. Model-View-ViewModel (MVVM) View • Subscribe and present data from view model • Handle user actions (e.g. two-way binding) Model • Handle data and business logic • Talk to network / storage ViewModel • Bind data between model and view • Manage "UI states" • Subscribe states • Forward user actions • Read / store data • Subscribe changes
  62. 62. MVVM in iOS 13 • View: SwiftUI • ViewModel: Bindable Object and Combine • Model: existing SDK features (URLSession, Core Model, etc.) • Communication: subscription via Combine
  63. 63. SwiftUI as View struct SearchView: View { @EnvironmentObject var model: SearchViewModel var body: some View { VStack { TextField("Search Wiki...", text: $model.searchText) if model.searchResult.count > 0 { List(model.searchResult) { result in NavigationLink(destination: SearchResultDetail(searchResult: result)) { Text(result.name) } } } else { Spacer() Text("No Results") } } } }
  64. 64. ObservableObject as ViewModel class SearchViewModel: ObservableObject { private let searchRepository: SearchRepository @Published var searchResult: [SearchResultItem] = [] @Published var searchText: String = "" // ... init(searchRepository: SearchRepository) { self.searchRepository = searchRepository $searchText .dropFirst(1) .debounce(for: 1, scheduler: RunLoop.main) // ... .flatMap { searchText in self.searchRepository.search(by: searchText, limit: self.limit) } // ... .sink { result in self.searchResult = result } .store(in: &cancellable) } }
  65. 65. MVVM Flow Example SearchView SearchViewModel SearchRepository (model) User keys in texts TextField changes searchText value (via binding) Transforms searchText into search keyword Fetches Wikipedia search data with keyword Parses search results Sets result to searchResult Invalidate view
  66. 66. Conclusion • Adapt SwiftUI for declarative view structure • Use Combine to handle asynchronous flows and event streams • Implement MVVM with SwiftUI and Combine • Write less codes, but more concise and predictable
  67. 67. WWDC 2019 References • 204 - Introducing SwiftUI: Building Your First App • 216 - SwiftUI Essentials • 226 - Data Flow Through SwiftUI • 721 - Combine in Practice • 722 - Introducing Combine * Some APIs have been renamed since between WWDC and official release
  68. 68. References • https://developer.apple.com/documentation/swiftui • https://developer.apple.com/documentation/combine • https://github.com/teaualune/swiftui_example_wiki_search • https://github.com/heckj/swiftui-notes • https://www.raywenderlich.com/4161005-mvvm-with- combine-tutorial-for-ios

A brief wrap up and introduction to SwiftUI and Combine brought by Apple in WWDC 2019, as well as utilizing both to propose a View-Model-ViewModel (MVVM) structure.

Views

Total views

8,092

On Slideshare

0

From embeds

0

Number of embeds

5,633

Actions

Downloads

35

Shares

0

Comments

0

Likes

0

×