The document discusses building reusable SwiftUI components. It demonstrates refactoring a book browsing UI to extract reusable subviews using various SwiftUI techniques like @State, List, HStack and VStack. It shows how to extract subviews to reduce duplication and make the code more modular and reusable. Key refactorings discussed include extracting subviews, local subviews and functions to improve code organization and reuse.
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
⚠
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
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
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!
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
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