SlideShare ist ein Scribd-Unternehmen logo
1 von 99
Downloaden Sie, um offline zu lesen
Building Reusable SwiftUI
Components
Peter Friese, Developer Advocate, Firebase


@pete
rf
riese
Peter Friese
@pete
rf
riese
Developer Advocate, Firebase
Building Reusable SwiftUI
Components Why this talk?
Help developers succeed


by making it easy to build


and grow apps
Hello World!
Hello World!
import SwiftUI


struct ContentView: View {


var body: some View {


VStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text("Hello, world!")


}


}


}
Add some state
Hello World!
import SwiftUI


struct ContentView: View {


var body: some View {


VStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text("Hello, world!")


}


}


}
@State var books = Book.samples
Add some state
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


VStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text("Hello, world!")


}


}


}
♻ Embed in List
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(0
.
.
<
5) { item in


VStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text("Hello, world!")


}


}


}


}
♻ Embed in List
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(0
.
.
<
5) { item in


VStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text("Hello, world!")


}


}


}


}
Change to HStack
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(0
.
.
<
5) { item in


HStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text("Hello, world!")


}


}


}


}
Embed in List
Bind state
Change to HStack
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack {


Image(systemName: "globe")


.imageScale(.large)


.foregroundColor(.accentColor)


Text(book.title)


}


}


}


}
Embed in List
Bind state
Change to HStack
Use book image
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack {


Text(book.title)


}


}


}


}
Use book image
Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)
Hello World!
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


Text(book.title)


}


}


}


}
♻ Embed in VStack
Use book image
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack {


Text(book.title)


}


}


}


}


Hello World!
Use book image
Add more details
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack {


Text(book.title)


}




Hello World!
Use book image
Add more details
Fix alignments
.font(.headline)


Text("by book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}




Hello World!
Use book image
Add more details
Fix alignments
import SwiftUI


struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}




Hello World!
Use book image
Add more details
Fix alignments
⚠
DRY - Don’t repeat yourself
Useful SwiftUI Refactorings
♻ Extract Subview
♻ Extract local Subview
♻ Extract local function
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}


}


}


}


}
♻ Extract Subview
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView()


}


}


}


struct BookRowView: View {


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")




♻ Extract Subview
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView()


}


}


}


struct BookRowView: View {


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")




♻ Extract Subview
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView()


}


}


}


struct BookRowView: View {


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")




♻ Extract Subview
Refactorings
Extract to Subview for reusable
parts of the UI (and for a
cleaner structure)
❌ Cannot find ‘book’ in scope
❌ Cannot find ‘book’ in scope
❌ Cannot find ‘book’ in scope
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView()


}


}


}


struct BookRowView: View {


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")




♻ Extract Subview
❌ Cannot find ‘book’ in scope
❌ Cannot find ‘book’ in scope
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView()


}


}


}


struct BookRowView: View {


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)




♻ Extract Subview
var book: Book
❌ Cannot find ‘book’ in scope
❌ Cannot find ‘book’ in scope
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView(book: book)


}


}


}


struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)




♻ Extract Subview
Peter’s Wishlist
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
struct ContentView: View {


@State var books = Book.samples


var body: some View {


List(books) { book in


BookRowView(book: book)


}


}


}


struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}


}


}


}


?
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}


}


}


}


Spacer()
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}


}


}


}


Spacer()
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}


}


}


}


Spacer()
♻ Extract local Subview
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text(book.title)


.font(.headline)


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)




var titleLabel: some View {


}


♻ Extract local Subview
Text(book.title)


.font(.headline)
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)




var titleLabel: some View {


Text(book.title)


.font(.headline)


}


♻ Extract local Subview
Text(book.title)


.font(.headline)
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)




var titleLabel: some View {


Text(book.title)


.font(.headline)


}


♻ Extract local Subview
Text(book.title)


.font(.headline)
titleLabel
struct BookRowView: View {


var book: Book


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}




var titleLabel: some View {


Text(book.title)


.font(.headline)


}


♻ Extract local Subview
Text(book.title)


.font(.headline)
Peter’s Wishlist
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
Add Extract to local Subview
struct BookRowView: View {


var book: Book


var titleLabel: some View {


Text(book.title)


.font(.headline)


}


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}




♻ Extract local function
Text(book.title)


.font(.headline)


}


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


Text("by (book.author)")


.font(.subheadline)


Text("(book.pages) pages")


.font(.subheadline)


}




♻ Extract local function
func detailsLabel(_ text: String)
-
>
Text {


Text(text)


.font(.subheadline)


}
Text(book.title)


.font(.headline)


}


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


(“by (book.author)")


.font(.subheadline)


(“(book.pages) pages")


.font(.subheadline)


}




♻ Extract local function
func detailsLabel(_ text: String)
-
>
Text {


Text(text)


.font(.subheadline)


}
Text


Text
Text(book.title)


.font(.headline)


}


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


(“by (book.author)")


.font(.subheadline)


(“(book.pages) pages")


.font(.subheadline)


}




♻ Extract local function
func detailsLabel(_ text: String)
-
>
Text {


Text(text)


.font(.subheadline)


}
detailsLabel


detailsLabel
Text(book.title)


.font(.headline)


}


func detailsLabel(_ text: String)
-
>
Text {


Text(text)


.font(.subheadline)


}


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


detailsLabel(“by (book.author)")


detailsLabel(“(book.pages) pages")


}


Spacer()


}




♻ Extract local function
var titleLabel: some View {


Text(book.title)


.font(.headline)


}


func detailsLabel(_ text: String)
-
>
Text {


Text(text)


.font(.subheadline)


}


var body: some View {


HStack(alignment: .top) {


Image(book.mediumCoverImageName)


.resizable()


.aspectRatio(contentMode: .fit)


.frame(height: 90)


VStack(alignment: .leading) {


titleLabel


detailsLabel(“by (book.author)")


detailsLabel(“(book.pages) pages")


}


Spacer()




Refactorings
Extract to Subview for reusable
parts of the UI (and for a
cleaner structure)
Extract to local subview when
you need to access properties of
the parent view
Extract to local function when
you want to pass in values from
the local scope
Building a Reusable Text Input Field
With a floating label
TextInputField
✨ Drop-in replacement for TextField


✨ Mandatory fields


✨ Custom validation


✨ Floating label


✨ Styling options


✨ Focus handling


✨ Clear button
Drop-in replacement for TextField
TextField("First Name", text: $shippingAddress.firstName)
Original (TextField)
Drop-in (TextInputField)
TextInputField("First Name", text: $shippingAddress.firstName)
Drop-in replacement for TextField
/
/
/
Creates a text field with a text label generated from a title string.


/
/
/


/
/
/
- Parameters:


/
/
/
- title: The title of the text view, describing its purpose.


/
/
/
- text: The text to display and edit.


@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)


public init<S>(_ title: S, text: Binding<String>) where S : StringProtocol
Original (TextField)
Drop-in (TextInputField)
TextInputField("First Name", text: $shippingAddress.firstName)
/
/
/


/
/
/
- Parameters:


/
/
/
- title: The title of the text view, describing its purpose.


/
/
/
- text: The text to display and edit.


@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)


public init<S>(_ title: S, text: Binding<String>) where S : StringProtocol
Original (TextField)
struct TextInputField: View {


private var title: String


@Binding private var text: String




init(_ title: String, text: Binding<String>) {


self.title = title


self._text = text


}




var body: some View {


ZStack(alignment: .leading) {


Text(title)


TextField("", text: $text)


}




Drop-in (TextInputField)
Floating Label
struct TextInputField: View {


private var title: String


@Binding private var text: String




init(_ title: String, text: Binding<String>) {


self.title = title


self._text = text


}




var body: some View {


ZStack(alignment: .leading) {


Text(title)


TextField("", text: $text)


}


.padding(.top, 15)


.animation(.default)


}


Placeholder
TextField
Floating Label
struct TextInputField: View {


private var title: String


@Binding private var text: String




init(_ title: String, text: Binding<String>) {


self.title = title


self._text = text


}




var body: some View {


ZStack(alignment: .leading) {


Text(title)


TextField("", text: $text)


}


.padding(.top, 15)




.foregroundColor(text.isEmpty ?


Color(.placeholderText) : .accentColor)


Foreground color
struct TextInputField: View {


private var title: String


@Binding private var text: String




init(_ title: String, text: Binding<String>) {


self.title = title


self._text = text


}




var body: some View {


ZStack(alignment: .leading) {


Text(title)


.foregroundColor(text.isEmpty ?


Color(.placeholderText) : .accentColor)


TextField("", text: $text)


}


.padding(.top, 15)


.animation(.default)




Floating Label
.offset(y: text.isEmpty ? 0 : -25)
Offset
struct TextInputField: View {


private var title: String


@Binding private var text: String




init(_ title: String, text: Binding<String>) {


self.title = title


self._text = text


}




var body: some View {


ZStack(alignment: .leading) {


Text(title)


.foregroundColor(text.isEmpty ?


Color(.placeholderText) : .accentColor)


.offset(y: text.isEmpty ? 0 : -25)


TextField("", text: $text)


}


.padding(.top, 15)




Floating Label
scale
.scaleEffect(text.isEmpty ? 1: 0.8, anchor: .leading)
Clear Button
Floating Label
Clear Button
struct TextInputField: View {


var clearButton: some View {


HStack {


if !clearButtonHidden {


Spacer()


Button(action: { text = "" }) {


Image(systemName: "multiply.circle.fill")


.foregroundColor(Color(UIColor.systemGray))


}


}


else {


EmptyView()


}


}


}


var body: some View {




Inner view
EmptyView()


}


}


}


var body: some View {


ZStack(alignment: .leading) {


/
/
.
.
.


TextField("", text: $text)


.padding(.trailing, clearButtonPadding)


.overlay(clearButton)


}


.padding(.top, 15)


.animation(.default)


}


}
Clear Button
Prevent clipping
Customising Views
Customising Views
TextInputField("First Name", text: $vm.firstName)


.clearButtonHidden(false)


TextInputField("First Name", text: $vm.firstName)


.clearButtonHidden(true)
How?
extension View {


func clearButtonHidden(_ hidesClearButton: Bool = true)
-
>
some View {


environment(.clearButtonHidden, hidesClearButton)


}


}


private struct TextInputFieldClearButtonHidden: EnvironmentKey {


static var defaultValue: Bool = false


}


extension EnvironmentValues {


var clearButtonHidden: Bool {


get { self[TextInputFieldClearButtonHidden.self] }


set { self[TextInputFieldClearButtonHidden.self] = newValue }


}


}
Customising Views
Using the SwiftUI Environment
else {
/
/
.
.
.
}


}


}


}


extension View {


func clearButtonHidden(_ hidesClearButton: Bool = true)
-
>
some View {


environment(.clearButtonHidden, hidesClearButton)


}


}


private struct TextInputFieldClearButtonHidden: EnvironmentKey {


static var defaultValue: Bool = false


}


extension EnvironmentValues {


var clearButtonHidden: Bool {


get { self[TextInputFieldClearButtonHidden.self] }


set { self[TextInputFieldClearButtonHidden.self] = newValue }


}


}
Customising Views
Using the SwiftUI Environment
struct TextInputField: View {


@Environment(.clearButtonHidden) var clearButtonHidden


var clearButton: some View {


HStack {


if !clearButtonHidden {


/
/
.
.
.


}


else {
/
/
.
.
.
}


}


}


}


extension View {


func clearButtonHidden(_ hidesClearButton: Bool = true)
-
>
some View {


environment(.clearButtonHidden, hidesClearButton)


}


}


Customising Views
Using the SwiftUI Environment
Customising Views
var body: some View {


Form {


Section(header: Text("Shipping Address")) {


TextInputField("First Name", text: $vm.firstName)


TextInputField("Last Name", text: $vm.lastName)


TextInputField("Street", text: $vm.street)


TextInputField("Number", text: $vm.number)


.clearButtonHidden(false)


TextInputField("Post code", text: $vm.postcode)


TextInputField("City", text: $vm.city)


TextInputField("County", text: $vm.county)


TextInputField("Country", text: $vm.country)


.clearButtonHidden(false)


}


.clearButtonHidden(true)


}


}
Values trickle down through the environment
View styling
❓Can we still style or views?


❓What about view modifiers such as


disableAutocorrection or keyboardType?


❓Will we need to expose them all manually?
This all still works,
thanks to the
SwiftUI Environment!
View styling
var body: some View {


Form {


Section(header: Text("Shipping Address")) {


TextInputField("First Name", text: $vm.firstName)


.disableAutocorrection(true)


TextInputField("Last Name", text: $vm.lastName)


TextInputField("Street", text: $vm.street)


TextInputField("Number", text: $vm.number)


.keyboardType(.numberPad)


.clearButtonHidden(false)


TextInputField("Post code", text: $vm.postcode)


TextInputField("City", text: $vm.city)


TextInputField("County", text: $vm.county)


TextInputField("Country", text: $vm.country)


.clearButtonHidden(false)


}


.clearButtonHidden(true)




This all still works,
thanks to the
SwiftUI Environment!
🎯
What
about
focus
handling?
Focus handling
var body: some View {


@FocusState private var focus: FocusableField?


Form {


Section(header: Text("Shipping Address")) {


TextInputField("First Name", text: $vm.firstName)


.disableAutocorrection(true)


TextInputField("Last Name", text: $vm.lastName)


TextInputField("Street", text: $vm.street)


TextInputField("Number", text: $vm.number)


.keyboardType(.numberPad)


.clearButtonHidden(false)


TextInputField("Post code", text: $vm.postcode)


TextInputField("City", text: $vm.city)


TextInputField("County", text: $vm.county)


TextInputField("Country", text: $vm.country)


.clearButtonHidden(false)


}
Focus handling
enum FocusableField: Hashable {


case firstName


case lastName


}


var body: some View {


@FocusState private var focus: FocusableField?


Form {


Section(header: Text("Shipping Address")) {


TextInputField("First Name", text: $vm.firstName)


.disableAutocorrection(true)


TextInputField("Last Name", text: $vm.lastName)


TextInputField("Street", text: $vm.street)


TextInputField("Number", text: $vm.number)


.keyboardType(.numberPad)



.focused($focus, equals: .firstName)
.focused($focus, equals: .lastName)
Again, this works thanks to
the SwiftUI Environment
✅
Validation handling
✅ Validation handling
}


}


}


}


extension View {


func isMandatory(_ value: Bool = true)
-
>
some View {


environment(.isMandatory, value)


}


}


private struct TextInputFieldMandatory: EnvironmentKey {


static var defaultValue: Bool = false


}


extension EnvironmentValues {


var isMandatory: Bool {


get { self[TextInputFieldMandatory.self] }


set { self[TextInputFieldMandatory.self] = newValue }


}


}
✅ Validation handling Connecting to the
SwiftUI Environment
struct TextInputField: View {


var body: some View {


ZStack(alignment: .leading) {


Text(title)


/
/
.
.
.


TextField("", text: $text)




✅ Validation handling Connecting to the
SwiftUI Environment
@Environment(.isMandatory) var isMandatory
struct TextInputField: View {


@Environment(.isMandatory) var isMandatory


var body: some View {


ZStack(alignment: .leading) {


Text(title)


/
/
.
.
.


TextField("", text: $text)




✅ Validation handling Performing the
validation
@State private var isValid: Bool = true


@State var validationMessage: String = “"


fileprivate func validate(_ value: String) {


if isMandatory {


isValid = !value.isEmpty


validationMessage = isValid ? "" : "This is a mandatory field"


}


}
if isMandatory {


isValid = !value.isEmpty


validationMessage = isValid ? "" : "This is a mandatory field"


}


}


var body: some View {


ZStack(alignment: .leading) {


Text(title)


/
/
.
.
.


TextField("", text: $text)




✅ Validation handling Update the UI according
to the validation state
if !isValid {


Text(validationMessage)


.foregroundColor(.red)


.offset(y: -25)


.scaleEffect(0.8, anchor: .leading)


}
.onAppear {


validate(text)


}


.onChange(of: text) { value in


validate(value)


}


Trigger validation
✅ Validation handling - Exposing inner state
How can we expose the
view’s inner state to the
outside world?
✅ Validation handling - Exposing inner state
Form {


Section(header: errorLabel) {


TextInputField("Email address",


text: $viewModel.email,


isValid: $viewModel.isFormValid)


.isMandatory()


}


Section {


Button("Submit") {
.
.
.
}


.disabled(!viewModel.isFormValid)


}


}
✅ Validation handling - Exposing inner state
struct TextInputField: View {


@Binding private var isValidBinding: Bool


@State private var isValid: Bool = true




init(_ title: String, text: Binding<String>,


isValid isValidBinding: Binding<Bool>? = nil) {


self.title = title


self._text = text


self._isValidBinding = isValidBinding
?
?
.constant(true)


}


}
✅ Validation handling - Exposing inner state
struct TextInputField: View {


@Binding private var isValidBinding: Bool


@State private var isValid: Bool = true




init(_ title: String, text: Binding<String>,


isValid isValidBinding: Binding<Bool>? = nil) {


self.title = title


self._text = text


self._isValidBinding = isValidBinding
?
?
.constant(true)


}


}
{


didSet {


isValidBinding = isValid


}


}


Every change to isValid will
be assigned to the binding
✅ Validation handling - Custom Validation
How can we let the outside
world take part in the
validation process?
}


✅ Validation handling - Custom Validation
Form {


Section(header: errorLabel) {


TextInputField("Email address",


text: $viewModel.email,


isValid: $viewModel.isEmailValid)


.isMandatory()


.onValidate { value in


value.isEmail()


? .success(true)


: .failure(.init(message: "(value) is not a valid email address"))


}


.autocapitalization(.none)


}


}
Register a custom validation callback
Return success or failure
return NSLocalizedString("(message)", comment: "Message for generic
validation errors.")


}


}


private struct TextInputFieldValidationHandler: EnvironmentKey {


static var defaultValue: ((String)
-
>
Result<Bool, ValidationError>)?


}


extension EnvironmentValues {


var validationHandler: ((String)
-
>
Result<Bool, ValidationError>)? {


get { self[TextInputFieldValidationHandler.self] }


set { self[TextInputFieldValidationHandler.self] = newValue }


}


}


extension View {


func onValidate(validationHandler:


@escaping (String)
-
>
Result<Bool, ValidationError>)
-
>
some View {


environment(.validationHandler, validationHandler)


}


}


✅ How to register Closures / Callbacks
struct ValidationError: Error {


let message: String


}


extension ValidationError: LocalizedError {


public var errorDescription: String? {


return NSLocalizedString("(message)", comment: "Message for generic
validation errors.")


}


}


private struct TextInputFieldValidationHandler: EnvironmentKey {


static var defaultValue: ((String)
-
>
Result<Bool, ValidationError>)?


}


extension EnvironmentValues {


var validationHandler: ((String)
-
>
Result<Bool, ValidationError>)? {


get { self[TextInputFieldValidationHandler.self] }


set { self[TextInputFieldValidationHandler.self] = newValue }


}


}


✅ How to register Closures / Callbacks
struct TextInputField: View {


@Environment(.validationHandler) var validationHandler


fileprivate func validate(_ value: String) {


isValid = true


if isMandatory {


isValid = !value.isEmpty


validationMessage = isValid ? "" : "This is a mandatory field"


}


if isValid {


guard let validationHandler = self.validationHandler else { return }


let validationResult = validationHandler(value)


if case .failure(let error) = validationResult {


isValid = false


self.validationMessage = "(error.localizedDescription)"


}


else if case .success(let isValid) = validationResult {


self.isValid = isValid


self.validationMessage = ""


}


}




✅ How to register Closures / Callbacks
Call the custom handler
􀈈
Reusing the Component
􀈈
Reusing the Component locally
File > New Package
Add to the current project
♻ Extract into package
🤣
j/k - there is no
such refactoring
􀈈
Xcode Component Library
Component Library
/
/
MARK: - Component Library


public struct TextInputField_Library: LibraryContentProvider {


public var views: [LibraryItem] {


[LibraryItem(TextInputField("First Name", text:


.constant(“Peter")), title: "TextInputField", category: .control)]


}


public func modifiers(base: TextInputField)
-
>
[LibraryItem] {


[LibraryItem(base.clearButtonHidden(true), category: .control)]


}


}
😞
Peter’s Wishlist
Make Extract to Subview work all of the time
Extract to Subview: handle dependent
properties
Add Extract to File
Add Extract to local Subview
Add Extract to Package
Rich reviews for Xcode Component Library
􀈂
Sharing globally
import SwiftUI


struct ContentView: View {


var body: some View {


HStack {


Image(systemName: "at")


TextField("Email", text: $viewModel.email)


.textInputAutocapitalization(.never)


.disableAutocorrection(true)


.focused($focus, equals: .email)


.submitLabel(.next)


.onSubmit {


self.focus = .password


}


}


}


}
Drop-in replacement
Building a Reusable Text Input Field
✨Refactoring your SwiftUI code


✨Using view modifiers


✨Customising SwiftUI view appearance


✨Making use of the SwiftUI environment


✨Adding hooks for custom behaviour


✨Re-using locally


✨Using the Xcode Component library


✨Publishing to GitHub


✨Building drop-in replacements for built-in views
Thanks!
Peter Friese

h
tt
p://pete
rf
riese.dev

@pete
rf
riese


youtube.com/c/PeterFriese/
Follow me
🔗 https:
/
/
bit.ly/3n99fis
Resources
🔗 https:
/
/
bit.ly/3Of3Q5o
🔗 https:
/
/
www.fivestars.blog/

Weitere ähnliche Inhalte

Was ist angesagt?

Was ist angesagt? (20)

Javascript 101
Javascript 101Javascript 101
Javascript 101
 
Unidirectional Data Flow Through SwiftUI
Unidirectional Data Flow Through SwiftUIUnidirectional Data Flow Through SwiftUI
Unidirectional Data Flow Through SwiftUI
 
SwiftUI - Performance and Memory Management
SwiftUI - Performance and Memory ManagementSwiftUI - Performance and Memory Management
SwiftUI - Performance and Memory Management
 
Swift in SwiftUI
Swift in SwiftUISwift in SwiftUI
Swift in SwiftUI
 
State management in react applications (Statecharts)
State management in react applications (Statecharts)State management in react applications (Statecharts)
State management in react applications (Statecharts)
 
Introduction of Html/css/js
Introduction of Html/css/jsIntroduction of Html/css/js
Introduction of Html/css/js
 
Asynchronous JavaScript Programming with Callbacks & Promises
Asynchronous JavaScript Programming with Callbacks & PromisesAsynchronous JavaScript Programming with Callbacks & Promises
Asynchronous JavaScript Programming with Callbacks & Promises
 
JavaScript Promises
JavaScript PromisesJavaScript Promises
JavaScript Promises
 
Declarative UIs with Jetpack Compose
Declarative UIs with Jetpack ComposeDeclarative UIs with Jetpack Compose
Declarative UIs with Jetpack Compose
 
Javascript
JavascriptJavascript
Javascript
 
Html
HtmlHtml
Html
 
Php Tutorials for Beginners
Php Tutorials for BeginnersPhp Tutorials for Beginners
Php Tutorials for Beginners
 
What Is Php
What Is PhpWhat Is Php
What Is Php
 
JavaScript: Events Handling
JavaScript: Events HandlingJavaScript: Events Handling
JavaScript: Events Handling
 
CSS
CSSCSS
CSS
 
Workmanager PPTX
Workmanager PPTXWorkmanager PPTX
Workmanager PPTX
 
Flex box
Flex boxFlex box
Flex box
 
About Best friends - HTML, CSS and JS
About Best friends - HTML, CSS and JSAbout Best friends - HTML, CSS and JS
About Best friends - HTML, CSS and JS
 
View models and binding
View models and bindingView models and binding
View models and binding
 
JavaScript Tutorial For Beginners | JavaScript Training | JavaScript Programm...
JavaScript Tutorial For Beginners | JavaScript Training | JavaScript Programm...JavaScript Tutorial For Beginners | JavaScript Training | JavaScript Programm...
JavaScript Tutorial For Beginners | JavaScript Training | JavaScript Programm...
 

Ähnlich wie Building Reusable SwiftUI Components

Cascading Style Sheets
Cascading Style SheetsCascading Style Sheets
Cascading Style Sheets
Senthil Kumar
 

Ähnlich wie Building Reusable SwiftUI Components (20)

Arquitetando seu app Android com Jetpack
Arquitetando seu app Android com JetpackArquitetando seu app Android com Jetpack
Arquitetando seu app Android com Jetpack
 
Firebase for Apple Developers
Firebase for Apple DevelopersFirebase for Apple Developers
Firebase for Apple Developers
 
Firebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroesFirebase for Apple Developers - SwiftHeroes
Firebase for Apple Developers - SwiftHeroes
 
WordPress as the Backbone(.js)
WordPress as the Backbone(.js)WordPress as the Backbone(.js)
WordPress as the Backbone(.js)
 
Rapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and FirebaseRapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and Firebase
 
CSS for basic learner
CSS for basic learnerCSS for basic learner
CSS for basic learner
 
Svcc Building Rich Applications with Groovy's SwingBuilder
Svcc Building Rich Applications with Groovy's SwingBuilderSvcc Building Rich Applications with Groovy's SwingBuilder
Svcc Building Rich Applications with Groovy's SwingBuilder
 
GDSC FCU 第3堂 Flutter
GDSC FCU 第3堂 FlutterGDSC FCU 第3堂 Flutter
GDSC FCU 第3堂 Flutter
 
Aplicacoes dinamicas Rails com Backbone
Aplicacoes dinamicas Rails com BackboneAplicacoes dinamicas Rails com Backbone
Aplicacoes dinamicas Rails com Backbone
 
Voyager: The Widget Router
Voyager: The Widget RouterVoyager: The Widget Router
Voyager: The Widget Router
 
Rapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and FirebaseRapid Application Development with SwiftUI and Firebase
Rapid Application Development with SwiftUI and Firebase
 
Android query
Android queryAndroid query
Android query
 
Implementing CSS support for React Native
Implementing CSS support for React NativeImplementing CSS support for React Native
Implementing CSS support for React Native
 
Vue routing tutorial getting started with vue router
Vue routing tutorial getting started with vue routerVue routing tutorial getting started with vue router
Vue routing tutorial getting started with vue router
 
The Creative New World of CSS
The Creative New World of CSSThe Creative New World of CSS
The Creative New World of CSS
 
jQuery Essentials
jQuery EssentialsjQuery Essentials
jQuery Essentials
 
Lecture-8.pptx
Lecture-8.pptxLecture-8.pptx
Lecture-8.pptx
 
Cascading Style Sheets
Cascading Style SheetsCascading Style Sheets
Cascading Style Sheets
 
IN4308 Lecture 3
IN4308 Lecture 3IN4308 Lecture 3
IN4308 Lecture 3
 
CPOSC Talk - The Gatsby, Contentful, Netlify Stack
CPOSC Talk - The Gatsby, Contentful, Netlify StackCPOSC Talk - The Gatsby, Contentful, Netlify Stack
CPOSC Talk - The Gatsby, Contentful, Netlify Stack
 

Mehr von Peter Friese

Do Androids Dream of Electric Sheep
Do Androids Dream of Electric SheepDo Androids Dream of Electric Sheep
Do Androids Dream of Electric Sheep
Peter Friese
 

Mehr von Peter Friese (20)

Firebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopFirebase & SwiftUI Workshop
Firebase & SwiftUI Workshop
 
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 +  = ❤️ (Firebase for Apple Developers) at Swift Leeds
 
async/await in Swift
async/await in Swiftasync/await in Swift
async/await in Swift
 
Building Apps with SwiftUI and Firebase
Building Apps with SwiftUI and FirebaseBuilding Apps with SwiftUI and Firebase
Building Apps with SwiftUI and Firebase
 
6 Things You Didn't Know About Firebase Auth
6 Things You Didn't Know About Firebase Auth6 Things You Didn't Know About Firebase Auth
6 Things You Didn't Know About Firebase Auth
 
Five Things You Didn't Know About Firebase Auth
Five Things You Didn't Know About Firebase AuthFive Things You Didn't Know About Firebase Auth
Five Things You Didn't Know About Firebase Auth
 
Building High-Quality Apps for Google Assistant
Building High-Quality Apps for Google AssistantBuilding High-Quality Apps for Google Assistant
Building High-Quality Apps for Google Assistant
 
Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google
 
Building Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on GoogleBuilding Conversational Experiences with Actions on Google
Building Conversational Experiences with Actions on Google
 
What's new in Android Wear 2.0
What's new in Android Wear 2.0What's new in Android Wear 2.0
What's new in Android Wear 2.0
 
Google Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinGoogle Fit, Android Wear & Xamarin
Google Fit, Android Wear & Xamarin
 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android Wear
 
Google Play Services Rock
Google Play Services RockGoogle Play Services Rock
Google Play Services Rock
 
Introduction to Android Wear
Introduction to Android WearIntroduction to Android Wear
Introduction to Android Wear
 
Google+ for Mobile Apps on iOS and Android
Google+ for Mobile Apps on iOS and AndroidGoogle+ for Mobile Apps on iOS and Android
Google+ for Mobile Apps on iOS and Android
 
Cross-Platform Authentication with Google+ Sign-In
Cross-Platform Authentication with Google+ Sign-InCross-Platform Authentication with Google+ Sign-In
Cross-Platform Authentication with Google+ Sign-In
 
Bring Back the Fun to Testing Android Apps with Robolectric
Bring Back the Fun to Testing Android Apps with RobolectricBring Back the Fun to Testing Android Apps with Robolectric
Bring Back the Fun to Testing Android Apps with Robolectric
 
Do Androids Dream of Electric Sheep
Do Androids Dream of Electric SheepDo Androids Dream of Electric Sheep
Do Androids Dream of Electric Sheep
 
Java based Cross-Platform Mobile Development
Java based Cross-Platform Mobile DevelopmentJava based Cross-Platform Mobile Development
Java based Cross-Platform Mobile Development
 
CouchDB Mobile - From Couch to 5K in 1 Hour
CouchDB Mobile - From Couch to 5K in 1 HourCouchDB Mobile - From Couch to 5K in 1 Hour
CouchDB Mobile - From Couch to 5K in 1 Hour
 

Kürzlich hochgeladen

CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
9953056974 Low Rate Call Girls In Saket, Delhi NCR
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
VishalKumarJha10
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
mohitmore19
 

Kürzlich hochgeladen (20)

Define the academic and professional writing..pdf
Define the academic and professional writing..pdfDefine the academic and professional writing..pdf
Define the academic and professional writing..pdf
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Models
 
Exploring the Best Video Editing App.pdf
Exploring the Best Video Editing App.pdfExploring the Best Video Editing App.pdf
Exploring the Best Video Editing App.pdf
 
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
 
How To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsHow To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.js
 
Microsoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdfMicrosoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdf
 
10 Trends Likely to Shape Enterprise Technology in 2024
10 Trends Likely to Shape Enterprise Technology in 202410 Trends Likely to Shape Enterprise Technology in 2024
10 Trends Likely to Shape Enterprise Technology in 2024
 
Diamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with PrecisionDiamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with Precision
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
 
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdfAzure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
 
8257 interfacing 2 in microprocessor for btech students
8257 interfacing 2 in microprocessor for btech students8257 interfacing 2 in microprocessor for btech students
8257 interfacing 2 in microprocessor for btech students
 
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
 
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
 
5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf
 
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS LiveVip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
 
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdfLearn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
 

Building Reusable SwiftUI Components

  • 1. Building Reusable SwiftUI Components Peter Friese, Developer Advocate, Firebase @pete rf riese
  • 4. Help developers succeed 
 by making it easy to build 
 and grow apps
  • 5.
  • 7. Hello World! import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } Add some state
  • 8. Hello World! import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } @State var books = Book.samples Add some state
  • 9. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } ♻ Embed in List
  • 10. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(0 . . < 5) { item in VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } } ♻ Embed in List
  • 11. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(0 . . < 5) { item in VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } } Change to HStack
  • 12. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(0 . . < 5) { item in HStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } } Embed in List Bind state Change to HStack
  • 13. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text(book.title) } } } } Embed in List Bind state Change to HStack Use book image
  • 14. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack { Text(book.title) } } } } Use book image Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90)
  • 15. Hello World! import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) Text(book.title) } } } } ♻ Embed in VStack Use book image
  • 16. import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack { Text(book.title) } } } } Hello World! Use book image Add more details
  • 17. import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack { Text(book.title) } Hello World! Use book image Add more details Fix alignments .font(.headline) Text("by book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline)
  • 18. import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } Hello World! Use book image Add more details Fix alignments
  • 19. import SwiftUI struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } Hello World! Use book image Add more details Fix alignments ⚠
  • 20. DRY - Don’t repeat yourself
  • 21. Useful SwiftUI Refactorings ♻ Extract Subview ♻ Extract local Subview ♻ Extract local function
  • 22. struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } } } } } ♻ Extract Subview
  • 23. struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView() } } } struct BookRowView: View { var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") ♻ Extract Subview
  • 24. struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView() } } } struct BookRowView: View { var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") ♻ Extract Subview
  • 25. struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView() } } } struct BookRowView: View { var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") ♻ Extract Subview Refactorings Extract to Subview for reusable parts of the UI (and for a cleaner structure)
  • 26. ❌ Cannot find ‘book’ in scope ❌ Cannot find ‘book’ in scope ❌ Cannot find ‘book’ in scope struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView() } } } struct BookRowView: View { var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") ♻ Extract Subview
  • 27. ❌ Cannot find ‘book’ in scope ❌ Cannot find ‘book’ in scope struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView() } } } struct BookRowView: View { var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) ♻ Extract Subview var book: Book
  • 28. ❌ Cannot find ‘book’ in scope ❌ Cannot find ‘book’ in scope struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView(book: book) } } } struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) ♻ Extract Subview
  • 29. Peter’s Wishlist Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File
  • 30. struct ContentView: View { @State var books = Book.samples var body: some View { List(books) { book in BookRowView(book: book) } } } struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline)
  • 31. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } } } } ?
  • 32. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } } } } Spacer()
  • 33. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } } } } Spacer()
  • 34. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } } } } Spacer() ♻ Extract local Subview
  • 35. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text(book.title) .font(.headline) Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) var titleLabel: some View { } ♻ Extract local Subview Text(book.title) .font(.headline)
  • 36. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) var titleLabel: some View { Text(book.title) .font(.headline) } ♻ Extract local Subview Text(book.title) .font(.headline)
  • 37. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) var titleLabel: some View { Text(book.title) .font(.headline) } ♻ Extract local Subview Text(book.title) .font(.headline) titleLabel
  • 38. struct BookRowView: View { var book: Book var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } var titleLabel: some View { Text(book.title) .font(.headline) } ♻ Extract local Subview Text(book.title) .font(.headline)
  • 39. Peter’s Wishlist Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File Add Extract to local Subview
  • 40. struct BookRowView: View { var book: Book var titleLabel: some View { Text(book.title) .font(.headline) } var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } ♻ Extract local function
  • 41. Text(book.title) .font(.headline) } var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel Text("by (book.author)") .font(.subheadline) Text("(book.pages) pages") .font(.subheadline) } ♻ Extract local function func detailsLabel(_ text: String) - > Text { Text(text) .font(.subheadline) }
  • 42. Text(book.title) .font(.headline) } var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel (“by (book.author)") .font(.subheadline) (“(book.pages) pages") .font(.subheadline) } ♻ Extract local function func detailsLabel(_ text: String) - > Text { Text(text) .font(.subheadline) } Text Text
  • 43. Text(book.title) .font(.headline) } var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel (“by (book.author)") .font(.subheadline) (“(book.pages) pages") .font(.subheadline) } ♻ Extract local function func detailsLabel(_ text: String) - > Text { Text(text) .font(.subheadline) } detailsLabel detailsLabel
  • 44. Text(book.title) .font(.headline) } func detailsLabel(_ text: String) - > Text { Text(text) .font(.subheadline) } var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel detailsLabel(“by (book.author)") detailsLabel(“(book.pages) pages") } Spacer() } ♻ Extract local function
  • 45. var titleLabel: some View { Text(book.title) .font(.headline) } func detailsLabel(_ text: String) - > Text { Text(text) .font(.subheadline) } var body: some View { HStack(alignment: .top) { Image(book.mediumCoverImageName) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 90) VStack(alignment: .leading) { titleLabel detailsLabel(“by (book.author)") detailsLabel(“(book.pages) pages") } Spacer() Refactorings Extract to Subview for reusable parts of the UI (and for a cleaner structure) Extract to local subview when you need to access properties of the parent view Extract to local function when you want to pass in values from the local scope
  • 46. Building a Reusable Text Input Field With a floating label
  • 47. TextInputField ✨ Drop-in replacement for TextField ✨ Mandatory fields ✨ Custom validation ✨ Floating label ✨ Styling options ✨ Focus handling ✨ Clear button
  • 48. Drop-in replacement for TextField TextField("First Name", text: $shippingAddress.firstName) Original (TextField) Drop-in (TextInputField) TextInputField("First Name", text: $shippingAddress.firstName)
  • 49. Drop-in replacement for TextField / / / Creates a text field with a text label generated from a title string. / / / / / / - Parameters: / / / - title: The title of the text view, describing its purpose. / / / - text: The text to display and edit. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public init<S>(_ title: S, text: Binding<String>) where S : StringProtocol Original (TextField) Drop-in (TextInputField) TextInputField("First Name", text: $shippingAddress.firstName)
  • 50. / / / / / / - Parameters: / / / - title: The title of the text view, describing its purpose. / / / - text: The text to display and edit. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public init<S>(_ title: S, text: Binding<String>) where S : StringProtocol Original (TextField) struct TextInputField: View { private var title: String @Binding private var text: String init(_ title: String, text: Binding<String>) { self.title = title self._text = text } var body: some View { ZStack(alignment: .leading) { Text(title) TextField("", text: $text) } Drop-in (TextInputField)
  • 51. Floating Label struct TextInputField: View { private var title: String @Binding private var text: String init(_ title: String, text: Binding<String>) { self.title = title self._text = text } var body: some View { ZStack(alignment: .leading) { Text(title) TextField("", text: $text) } .padding(.top, 15) .animation(.default) } Placeholder TextField
  • 52. Floating Label struct TextInputField: View { private var title: String @Binding private var text: String init(_ title: String, text: Binding<String>) { self.title = title self._text = text } var body: some View { ZStack(alignment: .leading) { Text(title) TextField("", text: $text) } .padding(.top, 15) .foregroundColor(text.isEmpty ? Color(.placeholderText) : .accentColor) Foreground color
  • 53. struct TextInputField: View { private var title: String @Binding private var text: String init(_ title: String, text: Binding<String>) { self.title = title self._text = text } var body: some View { ZStack(alignment: .leading) { Text(title) .foregroundColor(text.isEmpty ? Color(.placeholderText) : .accentColor) TextField("", text: $text) } .padding(.top, 15) .animation(.default) Floating Label .offset(y: text.isEmpty ? 0 : -25) Offset
  • 54. struct TextInputField: View { private var title: String @Binding private var text: String init(_ title: String, text: Binding<String>) { self.title = title self._text = text } var body: some View { ZStack(alignment: .leading) { Text(title) .foregroundColor(text.isEmpty ? Color(.placeholderText) : .accentColor) .offset(y: text.isEmpty ? 0 : -25) TextField("", text: $text) } .padding(.top, 15) Floating Label scale .scaleEffect(text.isEmpty ? 1: 0.8, anchor: .leading)
  • 56. Clear Button struct TextInputField: View { var clearButton: some View { HStack { if !clearButtonHidden { Spacer() Button(action: { text = "" }) { Image(systemName: "multiply.circle.fill") .foregroundColor(Color(UIColor.systemGray)) } } else { EmptyView() } } } var body: some View { Inner view
  • 57. EmptyView() } } } var body: some View { ZStack(alignment: .leading) { / / . . . TextField("", text: $text) .padding(.trailing, clearButtonPadding) .overlay(clearButton) } .padding(.top, 15) .animation(.default) } } Clear Button Prevent clipping
  • 59. Customising Views TextInputField("First Name", text: $vm.firstName) .clearButtonHidden(false) TextInputField("First Name", text: $vm.firstName) .clearButtonHidden(true) How?
  • 60. extension View { func clearButtonHidden(_ hidesClearButton: Bool = true) - > some View { environment(.clearButtonHidden, hidesClearButton) } } private struct TextInputFieldClearButtonHidden: EnvironmentKey { static var defaultValue: Bool = false } extension EnvironmentValues { var clearButtonHidden: Bool { get { self[TextInputFieldClearButtonHidden.self] } set { self[TextInputFieldClearButtonHidden.self] = newValue } } } Customising Views Using the SwiftUI Environment
  • 61. else { / / . . . } } } } extension View { func clearButtonHidden(_ hidesClearButton: Bool = true) - > some View { environment(.clearButtonHidden, hidesClearButton) } } private struct TextInputFieldClearButtonHidden: EnvironmentKey { static var defaultValue: Bool = false } extension EnvironmentValues { var clearButtonHidden: Bool { get { self[TextInputFieldClearButtonHidden.self] } set { self[TextInputFieldClearButtonHidden.self] = newValue } } } Customising Views Using the SwiftUI Environment
  • 62. struct TextInputField: View { @Environment(.clearButtonHidden) var clearButtonHidden var clearButton: some View { HStack { if !clearButtonHidden { / / . . . } else { / / . . . } } } } extension View { func clearButtonHidden(_ hidesClearButton: Bool = true) - > some View { environment(.clearButtonHidden, hidesClearButton) } } Customising Views Using the SwiftUI Environment
  • 63. Customising Views var body: some View { Form { Section(header: Text("Shipping Address")) { TextInputField("First Name", text: $vm.firstName) TextInputField("Last Name", text: $vm.lastName) TextInputField("Street", text: $vm.street) TextInputField("Number", text: $vm.number) .clearButtonHidden(false) TextInputField("Post code", text: $vm.postcode) TextInputField("City", text: $vm.city) TextInputField("County", text: $vm.county) TextInputField("Country", text: $vm.country) .clearButtonHidden(false) } .clearButtonHidden(true) } } Values trickle down through the environment
  • 64. View styling ❓Can we still style or views? ❓What about view modifiers such as 
 disableAutocorrection or keyboardType? ❓Will we need to expose them all manually? This all still works, thanks to the SwiftUI Environment!
  • 65. View styling var body: some View { Form { Section(header: Text("Shipping Address")) { TextInputField("First Name", text: $vm.firstName) .disableAutocorrection(true) TextInputField("Last Name", text: $vm.lastName) TextInputField("Street", text: $vm.street) TextInputField("Number", text: $vm.number) .keyboardType(.numberPad) .clearButtonHidden(false) TextInputField("Post code", text: $vm.postcode) TextInputField("City", text: $vm.city) TextInputField("County", text: $vm.county) TextInputField("Country", text: $vm.country) .clearButtonHidden(false) } .clearButtonHidden(true) This all still works, thanks to the SwiftUI Environment!
  • 67. Focus handling var body: some View { @FocusState private var focus: FocusableField? Form { Section(header: Text("Shipping Address")) { TextInputField("First Name", text: $vm.firstName) .disableAutocorrection(true) TextInputField("Last Name", text: $vm.lastName) TextInputField("Street", text: $vm.street) TextInputField("Number", text: $vm.number) .keyboardType(.numberPad) .clearButtonHidden(false) TextInputField("Post code", text: $vm.postcode) TextInputField("City", text: $vm.city) TextInputField("County", text: $vm.county) TextInputField("Country", text: $vm.country) .clearButtonHidden(false) }
  • 68. Focus handling enum FocusableField: Hashable { case firstName case lastName } var body: some View { @FocusState private var focus: FocusableField? Form { Section(header: Text("Shipping Address")) { TextInputField("First Name", text: $vm.firstName) .disableAutocorrection(true) TextInputField("Last Name", text: $vm.lastName) TextInputField("Street", text: $vm.street) TextInputField("Number", text: $vm.number) .keyboardType(.numberPad) .focused($focus, equals: .firstName) .focused($focus, equals: .lastName) Again, this works thanks to the SwiftUI Environment
  • 71. } } } } extension View { func isMandatory(_ value: Bool = true) - > some View { environment(.isMandatory, value) } } private struct TextInputFieldMandatory: EnvironmentKey { static var defaultValue: Bool = false } extension EnvironmentValues { var isMandatory: Bool { get { self[TextInputFieldMandatory.self] } set { self[TextInputFieldMandatory.self] = newValue } } } ✅ Validation handling Connecting to the SwiftUI Environment
  • 72. struct TextInputField: View { var body: some View { ZStack(alignment: .leading) { Text(title) / / . . . TextField("", text: $text) ✅ Validation handling Connecting to the SwiftUI Environment @Environment(.isMandatory) var isMandatory
  • 73. struct TextInputField: View { @Environment(.isMandatory) var isMandatory var body: some View { ZStack(alignment: .leading) { Text(title) / / . . . TextField("", text: $text) ✅ Validation handling Performing the validation @State private var isValid: Bool = true @State var validationMessage: String = “" fileprivate func validate(_ value: String) { if isMandatory { isValid = !value.isEmpty validationMessage = isValid ? "" : "This is a mandatory field" } }
  • 74. if isMandatory { isValid = !value.isEmpty validationMessage = isValid ? "" : "This is a mandatory field" } } var body: some View { ZStack(alignment: .leading) { Text(title) / / . . . TextField("", text: $text) ✅ Validation handling Update the UI according to the validation state if !isValid { Text(validationMessage) .foregroundColor(.red) .offset(y: -25) .scaleEffect(0.8, anchor: .leading) } .onAppear { validate(text) } .onChange(of: text) { value in validate(value) } Trigger validation
  • 75. ✅ Validation handling - Exposing inner state How can we expose the view’s inner state to the outside world?
  • 76. ✅ Validation handling - Exposing inner state Form { Section(header: errorLabel) { TextInputField("Email address", text: $viewModel.email, isValid: $viewModel.isFormValid) .isMandatory() } Section { Button("Submit") { . . . } .disabled(!viewModel.isFormValid) } }
  • 77. ✅ Validation handling - Exposing inner state struct TextInputField: View { @Binding private var isValidBinding: Bool @State private var isValid: Bool = true init(_ title: String, text: Binding<String>, isValid isValidBinding: Binding<Bool>? = nil) { self.title = title self._text = text self._isValidBinding = isValidBinding ? ? .constant(true) } }
  • 78. ✅ Validation handling - Exposing inner state struct TextInputField: View { @Binding private var isValidBinding: Bool @State private var isValid: Bool = true init(_ title: String, text: Binding<String>, isValid isValidBinding: Binding<Bool>? = nil) { self.title = title self._text = text self._isValidBinding = isValidBinding ? ? .constant(true) } } { didSet { isValidBinding = isValid } } Every change to isValid will be assigned to the binding
  • 79. ✅ Validation handling - Custom Validation How can we let the outside world take part in the validation process?
  • 80. } ✅ Validation handling - Custom Validation Form { Section(header: errorLabel) { TextInputField("Email address", text: $viewModel.email, isValid: $viewModel.isEmailValid) .isMandatory() .onValidate { value in value.isEmail() ? .success(true) : .failure(.init(message: "(value) is not a valid email address")) } .autocapitalization(.none) } } Register a custom validation callback Return success or failure
  • 81. return NSLocalizedString("(message)", comment: "Message for generic validation errors.") } } private struct TextInputFieldValidationHandler: EnvironmentKey { static var defaultValue: ((String) - > Result<Bool, ValidationError>)? } extension EnvironmentValues { var validationHandler: ((String) - > Result<Bool, ValidationError>)? { get { self[TextInputFieldValidationHandler.self] } set { self[TextInputFieldValidationHandler.self] = newValue } } } extension View { func onValidate(validationHandler: @escaping (String) - > Result<Bool, ValidationError>) - > some View { environment(.validationHandler, validationHandler) } } ✅ How to register Closures / Callbacks
  • 82. struct ValidationError: Error { let message: String } extension ValidationError: LocalizedError { public var errorDescription: String? { return NSLocalizedString("(message)", comment: "Message for generic validation errors.") } } private struct TextInputFieldValidationHandler: EnvironmentKey { static var defaultValue: ((String) - > Result<Bool, ValidationError>)? } extension EnvironmentValues { var validationHandler: ((String) - > Result<Bool, ValidationError>)? { get { self[TextInputFieldValidationHandler.self] } set { self[TextInputFieldValidationHandler.self] = newValue } } } ✅ How to register Closures / Callbacks
  • 83. struct TextInputField: View { @Environment(.validationHandler) var validationHandler fileprivate func validate(_ value: String) { isValid = true if isMandatory { isValid = !value.isEmpty validationMessage = isValid ? "" : "This is a mandatory field" } if isValid { guard let validationHandler = self.validationHandler else { return } let validationResult = validationHandler(value) if case .failure(let error) = validationResult { isValid = false self.validationMessage = "(error.localizedDescription)" } else if case .success(let isValid) = validationResult { self.isValid = isValid self.validationMessage = "" } } ✅ How to register Closures / Callbacks Call the custom handler
  • 86. File > New Package Add to the current project
  • 87. ♻ Extract into package 🤣 j/k - there is no such refactoring
  • 89.
  • 90. Component Library / / MARK: - Component Library public struct TextInputField_Library: LibraryContentProvider { public var views: [LibraryItem] { [LibraryItem(TextInputField("First Name", text: .constant(“Peter")), title: "TextInputField", category: .control)] } public func modifiers(base: TextInputField) - > [LibraryItem] { [LibraryItem(base.clearButtonHidden(true), category: .control)] } }
  • 91. 😞
  • 92. Peter’s Wishlist Make Extract to Subview work all of the time Extract to Subview: handle dependent properties Add Extract to File Add Extract to local Subview Add Extract to Package Rich reviews for Xcode Component Library
  • 94.
  • 95. import SwiftUI struct ContentView: View { var body: some View { HStack { Image(systemName: "at") TextField("Email", text: $viewModel.email) .textInputAutocapitalization(.never) .disableAutocorrection(true) .focused($focus, equals: .email) .submitLabel(.next) .onSubmit { self.focus = .password } } } } Drop-in replacement
  • 96.
  • 97. Building a Reusable Text Input Field ✨Refactoring your SwiftUI code ✨Using view modifiers ✨Customising SwiftUI view appearance ✨Making use of the SwiftUI environment ✨Adding hooks for custom behaviour ✨Re-using locally ✨Using the Xcode Component library ✨Publishing to GitHub ✨Building drop-in replacements for built-in views