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/

Building Reusable SwiftUI Components

  • 1.
    Building Reusable SwiftUI Components PeterFriese, Developer Advocate, Firebase @pete rf riese
  • 2.
  • 3.
  • 4.
    Help developers succeed 
 bymaking it easy to build 
 and grow apps
  • 6.
  • 7.
    Hello World! import SwiftUI structContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } } } Add some state
  • 8.
    Hello World! import SwiftUI structContentView: 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 structContentView: 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 structContentView: 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 structContentView: 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 structContentView: 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 structContentView: 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 structContentView: 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 structContentView: 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’trepeat 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 Extractto 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 Extractto 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: someView { 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: someView { 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: someView { 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: someView { 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 ReusableText Input Field With a floating label
  • 47.
    TextInputField ✨ Drop-in replacementfor TextField ✨ Mandatory fields ✨ Custom validation ✨ Floating label ✨ Styling options ✨ Focus handling ✨ Clear button
  • 48.
    Drop-in replacement forTextField TextField("First Name", text: $shippingAddress.firstName) Original (TextField) Drop-in (TextInputField) TextInputField("First Name", text: $shippingAddress.firstName)
  • 49.
    Drop-in replacement forTextField / / / 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)
  • 55.
  • 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: someView { ZStack(alignment: .leading) { / / . . . TextField("", text: $text) .padding(.trailing, clearButtonPadding) .overlay(clearButton) } .padding(.top, 15) .animation(.default) } } Clear Button Prevent clipping
  • 58.
  • 59.
    Customising Views TextInputField("First Name",text: $vm.firstName) .clearButtonHidden(false) TextInputField("First Name", text: $vm.firstName) .clearButtonHidden(true) How?
  • 60.
    extension View { funcclearButtonHidden(_ 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 westill 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!
  • 66.
  • 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
  • 69.
  • 70.
  • 71.
    } } } } extension View { funcisMandatory(_ 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
  • 84.
  • 85.
  • 86.
    File > NewPackage Add to the current project
  • 87.
    ♻ Extract intopackage 🤣 j/k - there is no such refactoring
  • 88.
  • 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 Extractto 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
  • 93.
  • 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
  • 97.
    Building a ReusableText 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
  • 98.
  • 99.