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.
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. SwiftUI
• Declarative
• Merges code and visual design, instead of separation (like
storyboard)
• Prevents complex UIViewController codes
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. 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. 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. 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. 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. 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. 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. 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. 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. 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
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. 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. @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. 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. class SearchViewModel: ObservableObject {
@Published var searchResult: [SearchResultItem] =
[]
@Published var searchText: String = ""
}
struct ContentView: View {
@ObservedObject var model = SearchViewModel()
}
ObservableObject and
@ObservedObject
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. 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
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. 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
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
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. 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. 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. MVVM in iOS 13
• View: SwiftUI
• ViewModel: Bindable Object and Combine
• Model: existing SDK features (URLSession, Core Model,
etc.)
• Communication: subscription via Combine
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. 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. 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. 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. 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