Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Integrate your app to modern world in Niigata
Search
d_date
October 11, 2019
Programming
0
680
Integrate your app to modern world in Niigata
2019/10/11 Mobile Crew Niigata
d_date
October 11, 2019
Tweet
Share
More Decks by d_date
See All by d_date
TCA Practice in 5 min
d_date
2
1.6k
waiwai-swiftpm-part2
d_date
3
520
わいわいSwift PM part 1
d_date
2
410
What's new in Firebase 2021
d_date
2
1.5k
CI/CDをミニマルに構築する
d_date
1
580
Swift Package centered project - Build and Practice
d_date
20
15k
How to write Great Proposal
d_date
4
1.7k
Thinking about Architecture for SwiftUI
d_date
8
2.4k
Integrate your app to modern world
d_date
2
670
Other Decks in Programming
See All in Programming
Terraform やるなら公式スタイルガイドを読もう 〜重要項目 10選〜
hiyanger
11
2.8k
Go製CLIツールをnpmで配布するには
syumai
2
1k
バイブスあるコーディングで ~PHP~ 便利ツールをつくるプラクティス
uzulla
1
310
Reactの歴史を振り返る
tutinoko
1
160
Quality Gates in the Age of Agentic Coding
helmedeiros
PRO
1
120
Google I/O Extended Incheon 2025 ~ What's new in Android development tools
pluu
1
220
0から始めるモジュラーモノリス-クリーンなモノリスを目指して
sushi0120
0
250
知って得する@cloudflare_vite-pluginのあれこれ
chimame
1
140
[SRE NEXT] 複雑なシステムにおけるUser Journey SLOの導入
yakenji
1
890
バイブコーディングの正体——AIエージェントはソフトウェア開発を変えるか?
stakaya
5
650
Claude Code派?Gemini CLI派? みんなで比較LT会!_20250716
junholee
1
790
DataformでPythonする / dataform-de-python
snhryt
0
140
Featured
See All Featured
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
48
2.9k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
656
60k
Six Lessons from altMBA
skipperchong
28
3.9k
Optimising Largest Contentful Paint
csswizardry
37
3.4k
Mobile First: as difficult as doing things right
swwweet
223
9.8k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
47
9.6k
Code Reviewing Like a Champion
maltzj
524
40k
Practical Orchestrator
shlominoach
190
11k
VelocityConf: Rendering Performance Case Studies
addyosmani
332
24k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
357
30k
Docker and Python
trallard
45
3.5k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
10
1k
Transcript
Integrate your app to modern world Mobile Crew NIIGATA Daiki
Matsudate @d_date iOS Developer
Daiki Matsudate • Tokyo • iOS Developer from iOS 4
• Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖʯ
Daiki Matsudate • Tokyo • iOS Developer from iOS 4
• Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖʯ
Daiki Matsudate • Tokyo • iOS Developer from iOS 4
• Google Developers Expert for Firebase • Independent Developer • Sushi • Book: ʮiOSΞϓϦઃܭύλʔϯೖʯ
None
March, 18 - 20th, 2020 https://www.tryswift.co/
Released iOS 13
Released iOS 13.1
Released iOS 13.2 Beta 2
iPhone 11 Pro
None
None
None
https://twitter.com/ios_memes/status/1174273871983370240?s=21
The Age of Declarative UI
The Age of Declarative UI • iOS: SwiftUI • Android:
Jetpack Compose • ReactNative / Flutter
SwiftUI • UI Framework with declarative Syntax • Live Preview
in Xcode • Available for iOS, iPadOS, macOS, watchOS and tvOS • Using newest Swift features • Property wrapper • Function Builder • Opaque Result Type • Goodbye Storyboard / Xib
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif public protocol View { associatedtype Body : View var body: Self.Body { get } }
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: View {
NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: NavigationView<List<Speaker.ID, NavigationLink<SpeakerRow,
SpeakerDetail>>> { NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif Opaque Result Type
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif No Comma!
import SwiftUI struct SpeakerList: View { var body: some View
{ NavigationView { List(speakersData, id: \.id) { speaker in NavigationLink(destination: SpeakerDetail(speaker: speaker)) { SpeakerRow(speaker: speaker) } } .navigationBarTitle(Text("Speakers"), displayMode: .automatic) } } } #if DEBUG struct SpeakerList_Previews : PreviewProvider { static var previews: some View { Group { SpeakerList() .environment(\.colorScheme, .light) SpeakerList() .environment(\.colorScheme, .dark) } } } #endif Function Builders
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } }
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } }
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } } Property Wrapper wrappedValue: Value projectedValue: Wrapped<Value> Without $ With $
struct ContentView: View { @State var selectedIndex: Int = 0
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) TabView(selection: $selectedIndex) { SpeakerList().tabItem { Text("Speaker").tag(0) } ScheduleList().tabItem { Text("Schedule").tag(1) } SponsorList().tabItem { Text("Sponsor").tag(2) } Text("Other").tabItem { Text("Other").tag(3) } } } } } Property Wrapper Value Binding<Value> Without $ With $ cf. RxSwift.BehaviorRelay
SwiftUI • UI Framework with declarative Syntax • Live Preview
in Xcode • Available for iOS, iPadOS, macOS, watchOS and tvOS • Using newest Swift features • Property wrapper • Function Builder • Opaque Result Type
Available in iOS 13
Ready for SwiftUI
Small components Key Concept
Model View Controller
Massive View Controller
Massive View Controller • Business logic in View Controller •
Massive View Controller • Business logic in View Controller •
Many UI Components in one View Controller
Small Components • Use StackView as possible • Use Xib
as possible • Use UIViewController than UIView
Small Components • Use StackView as possible • Use Xib
as possible • Use UIViewController than UIView super.init(nibName: nil, bundle: nil)
Ex. Compositional Layout • Featured content • Other sections •
The order will be A/B testing
Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout
Vertical Stack view Horizontal CollectionView Vertical CollectionView Ex. Compositional Layout
VStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak
self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.widthAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } }
VStackViewController import UIKit open class VStackViewController: UIViewController { public let
scrollView: UIScrollView = .init() public let stackView: UIStackView = { let stackView: UIStackView = .init() stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 0 return stackView }() var components: [UIViewController] public init(components: [UIViewController]) { self.components = components super.init(nibName: nil, bundle: nil) }
VStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak
self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.widthAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } } Set Constraints all edges of view and width anchor
import UIKit final class HomeViewController: VStackViewController { private let featuredComponent
= HomeFeaturedCellViewController(dependencies: .init(onTap: { index in // transition logic })) private let rankingComponents = HomeRankingViewController() init() { super.init(components: [featuredComponent, rankingComponents]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = "Home" }
import UIKit final class HomeViewController: VStackViewController { private let featuredComponent
= HomeFeaturedCellViewController(dependencies: .init(onTap: { index in // transition logic })) private let rankingComponents = HomeRankingViewController() init() { super.init(components: [featuredComponent, rankingComponents]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = "Home" } Override initializer with child components
Ex. User Registration • Each form need to be validate
• Enable to tap “Done” when all form has valid • Show error below each form
Ex. User Registration Vertical Stack view FormViewController FormViewController FormViewController FormViewController
FormViewController class FormViewController: UIViewController { struct Dependency { let title:
String let placeholder: String let validation: (String) -> ValidationResult let textContentType: UITextContentType? let keyboardType: UIKeyboardType? let isSecureTextEntry: Bool init(title: String, placeholder: String, validation: @escaping (String) -> ValidationResult, textContentType: UITextContentType? = nil, keyboardType: UIKeyboardType? = nil, isSecureTextEntry: Bool = false) { self.title = title self.placeholder = placeholder self.validation = validation self.textContentType = textContentType self.keyboardType = keyboardType self.isSecureTextEntry = isSecureTextEntry } }
FormViewController @IBOutlet private var titleLabel: UILabel! @IBOutlet var textField: UITextField!
@IBOutlet private var errorLabel: UILabel! private let dependency: Dependency private let disposeBag = DisposeBag() private lazy var viewModel: FormViewModel = .init(text: self.textField.rx.text.orEmpty.asDriver(), validation: self.dependency.validation) var isValid: Driver<Bool> { return viewModel.validatedValue.map { $0.isValid } } init(dependency: Dependency) { self.dependency = dependency super.init(nibName: nil, bundle: nil) }
FormViewController override func viewDidLoad() { super.viewDidLoad() errorLabel.isHidden = true titleLabel.text
= dependency.title textField.placeholder = dependency.placeholder textField.textContentType = dependency.textContentType if let keyboardType = dependency.keyboardType { textField.keyboardType = keyboardType } viewModel.validatedValue .drive(errorLabel.rx.validationResult) .disposed(by: disposeBag) }
Stack in horizontal HStack!
HStackViewController import UIKit open class HStackViewController: UIViewController { public let
scrollView: UIScrollView = .init() public let stackView: UIStackView = { let stackView: UIStackView = .init() stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 0 return stackView }() var components: [UIViewController] public init(components: [UIViewController]) { self.components = components super.init(nibName: nil, bundle: nil) }
HStackViewController import UIKit open class HStackViewController: UIViewController { public let
scrollView: UIScrollView = .init() public let stackView: UIStackView = { let stackView: UIStackView = .init() stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 0 return stackView }() var components: [UIViewController] public init(components: [UIViewController]) { self.components = components super.init(nibName: nil, bundle: nil) } Set to horizontal Scroll View Own child view controllers
HStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak
self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.heightAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } }
HStackViewController override open func viewDidLoad() { super.viewDidLoad() components.forEach { [weak
self] in self?.addChild($0) } view.addSubview(scrollView, constraints: .allEdges()) scrollView.addSubview(stackView, constraints: .allEdges() + [equal(\.heightAnchor)]) components.forEach { [weak self] in guard let self = self else { return } self.stackView.addArrangedSubview($0.view) } components.forEach { [weak self] in guard let self = self else { return } $0.didMove(toParent: self) } } Set Constraints all edges of view and height anchor
final class RegisterContentViewController: VStackViewController { private let userNameComponent = FormViewController(
dependency: .init( title: "Nickname", placeholder: "Nickname", validation: FormValidationService.shared.validate(nickName:), textContentType: .givenName ) ) private let emailComponent = FormViewController( dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress ) ) Initialize components
init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components:
[expirationComponent, securityCodeComponent] ) ] ) } Initialize components
Gather validation results How to be enabled?
lazy var isValidateForms: Driver<Bool> = Driver.combineLatest( userNameComponent.isValid, emailComponent.isValid, creditCardComponent.isValid, securityCodeComponent.isValid,
expirationComponent.isValid ) { $0 && $1 && $2 && $3 && $4 } .distinctUntilChanged() Passing results isValidateForms .drive(onNext: { [weak self] in guard let self = self else { return } self.confirmViewController.set(isValid: $0) }) .disposed(by: disposeBag)
Gather validation results Enabled
Gather validation results Error Disabled
None
None
final class RegisterContentViewController: VStackViewController { private let userNameComponent = FormViewController(
dependency: .init( title: "Nickname", placeholder: "Nickname", validation: FormValidationService.shared.validate(nickName:), textContentType: .givenName ) ) private let emailComponent = FormViewController( dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress ) ) Initialize components in UIKit
final class RegisterContentViewController: VStackViewController { private let userNameView = UIHostingController(
rootView: FormView( dependency: .init( title: "Nickname", placeholder: "Nickname", validation: FormValidationService.shared.validate(nickName:), textContentType: .givenName ) )) private let emailView = UIHostingController( rootView: FormView( dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress ) )) Initialize components in SwiftUI
init() { super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components:
[expirationComponent, securityCodeComponent] ) ] ) } Initialize components in UIKit
init() { super.init( components: [ userNameView, emailView, creditCardView, HStackViewController( components:
[expirationView, securityCodeView] ) ] ) } Initialize components in SwiftUI
FormViewController.xib
FormViewController.xib VStack Title Text Field Error Label
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack
{ HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack
{ HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI For Light / Dark mode
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack
{ HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Stack
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack
{ HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Title Label
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack
{ HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Text Field
var body: some View { ZStack { Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack
{ HStack { Text(dependency.title) .font(Font.system(size: 12, weight: .medium, design: .default)) .foregroundColor(Color(UIColor.label)) Spacer() } TextField(dependency.placeholder, text: $viewModel.value) .font(.system(size: 14, weight: .medium, design: .default)) .textFieldStyle(RoundedBorderTextFieldStyle()) .textContentType(dependency.textContentType) .keyboardType(dependency.keyboardType ?? .default) if !viewModel.isValid && !viewModel.isEmpty { HStack { Text(viewModel.errorMessage) .foregroundColor(.red) .font(Font.system(size: 12, weight: .medium, design: .default)) Spacer() } } } .padding() } } FormView in SwiftUI Error label
FormView preview struct FormView_Previews: PreviewProvider { static var previews: some
View { let form = FormView(dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress)) return Group { form.environment(\.colorScheme, .light) form.environment(\.colorScheme, .dark) } .previewLayout(.fixed(width: 414, height: 129)) } }
FormView preview struct FormView_Previews: PreviewProvider { static var previews: some
View { let form = FormView(dependency: .init( title: "Email", placeholder: "Email", validation: FormValidationService.shared.validate(email:), textContentType: .emailAddress)) return Group { form.environment(\.colorScheme, .light) form.environment(\.colorScheme, .dark) } .previewLayout(.fixed(width: 414, height: 129)) } } Light Mode Dark Mode
FormView preview
Dataflow
Combine
Combine • Declarative Swift API • ඇಉظͳΠϕϯτΛܕͱͯ͠දݱ • ଟछଟ༷ͳԋࢉࢠͰΠϕϯτΛϋϯυϦϯά •
Reactive Framework by Apple
https://twitter.com/diegopetrucci/status/1135655480825655297
User Interaction SwiftUI Action State Mutation View Updates Render !
" ⏰ Publisher https://developer.apple.com/videos/play/wwdc2019/226/
Dataflow in FormView struct FormView: View { let dependency: FormViewController.Dependency
@ObservedObject var viewModel: FormViewSwiftUIModel init(dependency: FormViewController.Dependency) { self.dependency = dependency self.viewModel = .init(validation: dependency.validation) } var isValid: Bool { viewModel.isValid && !viewModel.isEmpty }
Dataflow in FormView struct FormView: View { let dependency: FormViewController.Dependency
@ObservedObject var viewModel: FormViewSwiftUIModel init(dependency: FormViewController.Dependency) { self.dependency = dependency self.viewModel = .init(validation: dependency.validation) } var isValid: Bool { viewModel.isValid && !viewModel.isEmpty } ViewModel with ObservedObject
Dataflow in FormView import Foundation import SwiftUI class FormViewSwiftUIModel: ObservableObject
{ let validation: (String) -> ValidationResult var value: String = "" { willSet { if newValue != value { validationResult = self.validation(newValue) } } } var validationResult: ValidationResult = .empty { willSet { objectWillChange.send() } }
Dataflow in FormView var validationResult: ValidationResult = .empty { willSet
{ switch newValue { case .failed(let message): errorMessage = message case .ok: isValid = true isEmpty = false case .empty: isValid = false isEmpty = true default: errorMessage = "" } objectWillChange.send() } }
https://developer.apple.com/videos/play/wwdc2019/226/ Input text $viewModel.value objectWillChange.send()
UIKit in SwiftUI
UIKit in SwiftUI • Use UIViewControllerRepresentable / UIViewRepresentable • Call
in SwiftUI View
UIKit in SwiftUI extension FormViewController: UIViewControllerRepresentable { typealias UIViewControllerType =
FormViewController func makeUIViewController( context: UIViewControllerRepresentableContext<FormViewController>) -> FormViewController { let formViewController = FormViewController(dependency: self.dependency) return formViewController } func updateUIViewController(_ uiViewController: FormViewController, context: UIViewControllerRepresentableContext<FormViewController>) { } func makeCoordinator() -> () { } }
UIKit in SwiftUI var body: some View { ZStack {
Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack { userNameComponent emailComponent creditCardComponent HStack { expirationComponent securityCodeComponent Spacer() } Spacer() } } }
UIKit in SwiftUI var body: some View { ZStack {
Color(UIColor.systemBackground) .edgesIgnoringSafeArea(.all) VStack { userNameComponent emailComponent creditCardComponent HStack { expirationComponent securityCodeComponent Spacer() } Spacer() } } } super.init( components: [ userNameComponent, emailComponent, creditCardComponent, HStackViewController( components: [expirationComponent, securityCodeComponent] )] )
struct RegisterContentView_Previews: PreviewProvider { static var previews: some View {
let content = RegisterContentView() return Group { NavigationView { content.environment(\.colorScheme, .light) } NavigationView { content.environment(\.colorScheme, .dark) } } } } UIKit in SwiftUI - Preview
Automatic Preview with UIHostController
Failed to load xib inside SwiftUI
Time to throw away xib
Use Stack view as possible
let stackView = UIStackView(arrangedSubviews: [titleLabel, textField, errorLabel]) stackView.axis = .vertical
stackView.spacing = 16 view.addSubview(stackView, constraints: .allEdges(margin: 16)) Use stack view instead of xib
Use stack view instead of xib
Recap - UIKit to SwiftUI • Use stack view as
possible • Some of SwiftUI feature still be broken • Sometime need to use UIKit instead • Keeping components small is key factor to migrate