- [Mapping from UIKit to SwiftUI](#Mapping from UIKit to SwiftUI)
- Components
- Attributes
- [Create a custom View](#Create a custom View)
- [Update Initial View](#Update Initial View)
- @State
- @Binding
- [Observable Objects](#Observable Objects)
- [Environment Objects](#Environment Objects)
- Navigation
- Preview
- Gestures
- Composition
- [Button styles](#Button styles)
- PickerStyle
- DatePickerStyle
- TextFieldStyle
- ToggleStyle
- NavigationViewStyle
- ListStyle
- [View modifiers](#View modifiers)
- UIViewRepresentable
- [UIViewControllerRepresentable] (#UIViewControllerRepresentable)
- [Example Projects](#Example Projects)
- Animation and transitions
- Advantages
- Disadvantages
- References
UITableView:ListUICollectionView: No SwiftUI equivalentUILabel:TextUITextField:TextFieldUITextFieldwithisSecureTextEntryset to true:SecureFieldUITextView: No SwiftUI equivalentUISwitch:ToggleUISlider:SliderUIButton:ButtonUINavigationController:NavigationViewUIAlertControllerwith style.alert:AlertUIAlertControllerwith style.actionSheet:ActionSheetUIStackViewwith horizontal axis:HStackUIStackViewwith vertical axis:VStackUIImageView:ImageUISegmentedControl:PickerwithSegmentedPickerStyleUIPicker:PickerwithWheelPickerStyleUIStepper:StepperUIDatePicker:DatePickerUITabBarController:TabViewNSAttributedString: Incompatible with SwiftUI; useTextinstead.
HStack: Horizontal stackVStack: Vertical stackZStack: Stacks the elements in front of each otherGroup: Just group views togetherForm: Like VStack, but make the views adjust properly for user inputsSpacer: Space between views
// stacks init
init(alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content)
// spacer init
init(minLength: CGFloat? = nil)Two options to present arrays:
// Option 1
List {
ForEach(myArray, id: \.self) { element in
// create the view for the element here
}
}
// Option 2
List (myArray, id: \.self) { element in
// create the view for the element here
}Obs: id is the path to a property that uniquely identifies the object. If the object implements the protocol Identifiable, there is no need for it.
Sections
List {
ForEach(myIdentifiableArray) { element in
Section(header: headerView(for: element), footer: footerView(for: element)) {
// section content here
ForEach(sectionData(for: element)) { item in
// create the item view
}
}
}
}Delete swipe action
Only works with forEach:
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onDelete(perform: delete)
}
...
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}Move rows Only works with forEach:
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onMove(perform: move)
}
...
func move(from source: IndexSet, to destination: Int) {
users.move(fromOffsets: source, toOffset: destination)
}init(_: StringProtocol)init<T>(_ title: StringProtocol, // placeholder
value: Binding<T>,
formatter: Formatter,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {})init(_ title: StringProtocol,
text: Binding<String>,
onCommit: @escaping () -> Void = {})init(isOn: Binding<Bool>, @ViewBuilder label: () -> Label)
init(_ configuration: ToggleStyleConfiguration)
init<S>(_ title: S, isOn: Binding<Bool>) where S : StringProtocolinit<V>(value: Binding<V>,
in bounds: ClosedRange<V> = 0...1,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
minimumValueLabel: ValueLabel,
maximumValueLabel: ValueLabel,
@ViewBuilder label: () -> Label)
where V: BinaryFloatingPoint, V.Stride : BinaryFloatingPoint
init<V>(value: Binding<V>,
in bounds: ClosedRange<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
minimumValueLabel: ValueLabel,
maximumValueLabel: ValueLabel,
@ViewBuilder label: () -> Label)
where V: BinaryFloatingPoint, V.Stride : BinaryFloatingPointinit(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
init<S>(_ title: S, action: @escaping () -> Void) where S : StringProtocolinit(@ViewBuilder content: () -> Content)init(title: Text,
message: Text? = nil,
dismissButton: Alert.Button? = nil)
init(title: Text,
message: Text? = nil,
primaryButton: Alert.Button,
secondaryButton: Alert.Button)Alert.Button types: default, cancel and destructive
init(title: Text,
message: Text? = nil,
buttons: [ActionSheet.Button] = [.cancel()])init(_ name: String, bundle: Bundle? = nil)
// label is used for accessibility
init(_ name: String, bundle: Bundle? = nil, label: Text)
init(_ cgImage: CGImage,
scale: CGFloat,
orientation: Image.Orientation = .up,
label: Text)init(selection: Binding<SelectionValue>,
label: Label,
@ViewBuilder content: () -> Content)
init<S>(_ title: S,
selection: Binding<SelectionValue>,
@ViewBuilder content: () -> Content)
where S : StringProtocol
// Obs: to edit style, use:
.pickerStyle(SegmentedPickerStyle())
.pickerStyle(WheelPickerStyle())
.pickerStyle(DatePickerStyle())init(onIncrement: (() -> Void)?,
onDecrement: (() -> Void)?,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
@ViewBuilder label: () -> Label)
init<S, V>(_ title: S,
value: Binding<V>,
in bounds: ClosedRange<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in })
where S : StringProtocol, V : Strideable
init<S>(_ title: S,
onIncrement: (() -> Void)?,
onDecrement: (() -> Void)?,
onEditingChanged: @escaping (Bool) -> Void = { _ in })
where S : StringProtocolinit(selection: Binding<Date>,
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date],
@ViewBuilder label: () -> Label)
init(selection: Binding<Date>,
in range: ClosedRange<Date>
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date],
@ViewBuilder label: () -> Label)TabView {
Text("First View")
.tabItem {
VStack {
Image(systemName: "1.circle")
Text("First")
}
}.tag(0)
Text("Second View")
.tabItem {
VStack {
Image(systemName: "2.circle")
Text("Second")
}
}.tag(1)
}
Creates popup menus using 3D Touch.
Text("Control Click Me")
.contextMenu {
Button(action: { print("added") } ) { Text("Add") }
Button(action: { print("removed") } ) { Text("Remove") }
}// MARK: - Text
.foregroundColor(_ color: Color?) // text color
.background(view:) // can use view or color
.font(_ font: Font?) // predefined or custom styles
.fontWeight(_ weight: Font.Weight?) // bold, light...
.bold()
.italic()
.strikethrough(_ active: Bool = true, color: Color? = nil)
.underline(_ active: Bool = true, color: Color? = nil)
.lineLimit(_ number: Int?) // infinity by default
.truncationMode(_ mode: Text.TruncationMode)
.multilineTextAlignment(.center)
.lineSpacing(50)
// MARK: - Image
.resizable()
.aspectRatio(mode:) // fit or fill
.clipShape(shape:) // Circle(), Rectangle()...
// add view over the image
// Ex: .overlay(Circle().stroke(Color.black, lineWidth: 6))
.overlay(_ overlay: View, alignment: Alignment = .center)
.border(_ content: ShapeStyle, width: CGFloat = 1)
.shadow(radius:)
// Content position relative to the upper left corner of the frame.
// The frame stays in the same place
.positon(x:, y:)
// Content offset
// The frame stays in the same place
.offSet(x:, y:)
// clipp to the size of the parent view
.clipped()
.rotationEffect(_ angle: Angle, anchor: UnitPoint = .center)
.scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center)
.scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center)
.scaleEffect(x: CGFloat = 0.0, y: CGFloat = 0.0, anchor: UnitPoint = .center)
.blur(radius: CGFloat, opaque: Bool = false)
.brightness(_ amount: Double)
.colorInvert()
.grayscale(_ amount: Double)
// MARK: - Any view
.padding(direction:) // all, leading, trailing...
.padding(direction:, value:) // value can be negative
.frame(width:,height:, alignment:) // Obs: Can be used with spacer to control its size
.frame(minWidth:,idealWidth:, maxWidth:,
minHeight:, idealHeight:, maxHeight:,
alignment:)
.edgesIgnoringSafeArea(edges:) // top, bottom...
.onAppear(perform: (()->Void)?)In SwiftUI, Views are just struts that implement a protocol View, which have a property body: some View.
struct MyView: View {
// criar propriedades da view aqui, como em uma struct normal
var body: some View {
// criar view
}
// criar métodos da view aqui, como em uma struct normal
}Just open SceneDelegate.swift and and update the method
scene(_, willConnectTo:, options:) to use yout view:
window.rootViewController = UIHostingController(rootView: MyView())Changes in properties @State make the view update itself automatically.
struct MyView: View {
@State var name = "name"
var body: some View {
VStack {
Text(name)
Button(action: {
self.name = "new name"
}) {
Text("Change name")
}
}
}
}It is used to pass properties by reference. For example, TextField receives a bindable object and, everytime the text changes, it updates the binded property, so the view that contains the TextField can listen for changes in this property and update itself.
struct MyView: View {
@State var name = "name"
var body: some View {
VStack {
Text(name)
// $ indicates binding
// When TextField is edited, the name is updated
// and the view reloads itself because of @State
TextField("placeholder", text: $name)
}
}
}In order to create a custom view with a binding property, like TextField, just use @Binding:
struct MyView: View {
@Binding var value: Bool
var body: some View {
Button(action {
self.value.toggle()
}) {
Text("Change value")
}
}
}Objects that can be ubserved by other views, making them update itself when the object changes. The difference between Observable Objects and State is that Observable Objects can be observed by many views at the same time.
import Combine
final class MyObservableObject: ObservableObject {
// as soon as a property marked as @Published changes
// SwiftUI rebuild all Views bound to the MyObservableObject
@Published private(set) var myValue: Bool = false
func updateValue() {
myValue.toggle()
}
}
struct MyObserverView: View {
@ObservedObject var object = MyObservedClass()
var body: some View {
if (object.myValue) {
// create view 1
} else {
// create view 2
}
}
}Instead of passing ObservableObjects in the view init, we can set it in the view environment. The environment is stored in a different memory region, and all subviews have access to it. So, if set some configuration in the view environment, all subviews will be configured as well.
// inject EnvironmentObject into Environment of our View hierarchy
MyView().environmentObject(MyObservableObject())SwiftUI also have a predefined environment with some system configurations (check EnvironmentValues extensions), that can be observed using @Environment properties.
struct MyView: View {
@Environment(\.calendar) var calendar: Calendar
@Environment(\.locale) var locale: Locale
@Environment(\.colorScheme) var colorScheme: ColorScheme
@Environment(\.sizeCategory) var sizeCategory
@Environment(\.isEnabled) var isEnabled
@Environment(\.editMode) var editMode
@Environment(\.presentationMode) var presentationMode
}Note: You can update the environment of the root view, and the changes will be applied to all views in the project:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window = UIWindow(windowScene: scene as! UIWindowScene)
window?.rootViewController = UIHostingController(
rootView: RootView()
.environment(\.multilineTextAlignment, .center)
.environment(\.lineSpacing, 8)
)
window?.makeKeyAndVisible()
}- Use
NavigationLink(destination:, label:): It's a button that navigates to the given destination when touched - Obs: The
NavigationLinkshould be inside aNavigationViewto work NavigationViewis like aUINavigationController, so we just need aNavigationViewin the first screen.- To update the navbar title, sets the property
.navigationBarTitle()in any view inside theNavigationView - To set navbar items, use
.navigationBarItems(leading: View, trailing: View)in any view inside theNavigationView
Ex:
NavigationView {
VStack {
NavigationLink(destination: MyDestinationView()) {
Text("NavigationLink name")
}
}.navigationBarTitle(Text("MyTitle"))
}To present modal views use .sheet(isPresented: Binding<Bool>, content: () -> View)
struct MyView: View {
@State var shouldPresentView: Bool
var body: some View {
Button(action {
shouldPresentView = true
}) {
Text("PresentView")
}.sheet(isPresented: $shouldPresentView) { () -> View in
return MyPopoverView()
}
}
}To present alerts, use .alert(isPresented: Binding<Bool>, content: () -> View).
struct MyView: View {
@State var shouldPresentAlert: Bool
var body: some View {
VStack {
...
}.alert($shouldPresentAlert) {
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .default(Text("Button"), onTrigger: nil),
secondaryButton: .destructive(Text("Button 2"), onTrigger: nil))
}
}
}To dismiss some view, you can use the presentationMode from the environment:
struct ModalView: View {
@Environment(\.presentationMode) var presentation
var body: some View {
Button("dismiss") {
// use current view specific environment values to dismiss presented modal view.
self.presentation.wrappedValue.dismiss()
}
}
}You can change the preview to adjust to the view content, instead of showing the device:
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView().previewLayout(.sizeThatFits)
}
}You can also set a fixed preview size with fixed(width:, height)
You can have more tha one preview at the same time:
struct MyView_Previews: PreviewProvider {
static var previews: some View {
Group {
MyView().previewLayout(.sizeThatFits)
MyView().previewLayout(.sizeThatFits)
}
}
}struct MyView_Previews: PreviewProvider {
static var previews: some View {
Group {
MyView().previewDevice("iPhone 8")
MyView().previewDevice("iPhone XR")
.previewDisplayName("iPhone XR") // name below the simulator
}
}
}You can use the environment to preview with different accessibility configurations:
struct MyView_Previews: PreviewProvider {
static var previews: some View {
Group {
MyView().previewLayout(.sizeThatFits)
.environment(\.sizeCategory, .extraExtraExtraLarge)
MyView().previewLayout(.sizeThatFits)
.environment(\.sizeCategory, .extraSmall)
}
}
}view.gesture(
TapGesture(count: 1) // count = num of touches to trigger gesture
.onEnded({ _ in
// action
})
)
view.gesture(LongPressGesture(...))
view.gesture(SwipeGesture(...))- BorderlessButtonStyle
- DefaultButtonStyle
Create new style:
struct OutlineStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(minWidth: 44, minHeight: 44)
.padding(.horizontal)
.foregroundColor(Color.accentColor)
.background(RoundedRectangle(cornerRadius: 8).stroke(Color.accentColor))
}
}
Button(...)
.buttonStyle(OutlineStyle())- SegmentedPickerStyle
- WheelPickerStyle
- DefaultPickerStyle
- WheelDatePickerStyle
- DefaultDatePickerStyle
- RoundedBorderTextFieldStyle
- PlainTextFieldStyle
- DefaultTextFieldStyle
- SwitchToggleStyle
- DefaultToggleStyle
- StackNavigationViewStyle
- DoubleColumnNavigationViewStyle // master-detail
- PlainListStyle
- GroupedListStyle
struct TitleStyle: ViewModifier {
func body(content: Content) -> some View {
content
.font(.title)
.lineSpacing(8)
.foregroundColor(.primary)
}
}
extension Text {
func textStyle<Style: ViewModifier>(_ style: Style) -> ModifiedContent<Self, Style> {
ModifiedContent(content: self, modifier: style)
}
}
Text("title")
.textStyle(TitleStyle())
// Option 2: without Text extension
Text("title")
.modifier(TitleStyle())Protocol to convert SwiftUI views to UIKit views
struct MyView: UIViewRepresentable {
func makeUIView(context: MyView.UIViewRepresentableContext<MyView.UIViewType>) -> MyView.UIViewType {
// TODO: Create and return an instance of the corresponding UIView for this SwiftUI view
}
func updateUIView(_ uiView: MyView.UIViewType, context: MyView.UIViewRepresentableContext<MyView.UIViewType>) {
// TODO: Update the corresponding UIView
}
}To create views that represents UIViewControllers
struct PMyViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIViewController {
// Create the view controller here
}
func updateUIViewController(_ pageViewController: UIPageViewController,
context: Context) {
// update view Controller here
}
/// A SwiftUI view that represents a UIKit view controller
/// can define a Coordinator type that SwiftUI manages
/// and provides as part of the representable view’s context.
class Coordinator: NSObject {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
}
/// SwiftUI calls this makeCoordinator() method
/// before makeUIViewController(context:),
/// so that you have access to the coordinator object
/// when configuring your view controller.
/// - NOTE: You can use this coordinator to implement
/// common Cocoa patterns, such as delegates, data sources,
/// and responding to user events via target-action.
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}To add animation to a view, just use .animation():
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.padding()
.animation(.easeInOut()) // animate the rotation effectAnimation types:
.easeInOut(duration: Double)
.easeIn(duration: Double)
.easeOut(duration: Double)
.linear(duration: Double)
.timingCurve(_ c0x: Double, _ c0y: Double, _ c1x: Double, _ c1y: Double, duration: Double = 0.35)
.spring(response: Double = 0.55, dampingFraction: Double = 0.825, blendDuration: Double = 0)Animate @state properties change (animates all views that depends on the property)
withAnimation {
self.showDetail.toggle()
}Animate changes on binded properties:
Toggle(isOn: $showingWelcome.animation(.spring())) {
Text("Toggle label")
}Edit animations:
.speed(_ speed: Double)
.delay(_ delay: Double)
.repeatCount(_ repeatCount: Int, autoreverses: Bool = true)
.repeatForever(autoreverses: Bool = true)Cancel animations:
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(nil) // rotation effect will not be animated
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
.animation(.spring())Animated transitions:
if showDetail {
HikeDetail(hike: hike)
.transition(.slide)
}Transitions types:
// inserts by moving in from the leading edge,
// and removes by moving out towards the trailing edge.
.slide
/// A transition from transparent to opaque on insertion
/// and opaque to transparent on removal.
.opacity
.scale
.scale(scale: CGFloat, anchor: UnitPoint = .center)
/// moves the view away towards the specified `edge`
.move(edge: Edge)
/// composite `Transition` that uses a different transition for
/// insertion and removal.
.asymmetric(insertion: AnyTransition, removal: AnyTransition)Combine transitions together:
.combined(with other: AnyTransition)Create new transitions:
extension AnyTransition {
static var myTransition: AnyTransition {
// Combine the transitions you want here
}
}- Preview: see the changes effect immediately
- No UIViewController, UIView, constraints and Storyboard
- Easy to customize images (shadow, overlay, radius...)
- Easy to create gestures, animation and actions
- Easy to update tablevies: No need to register cell, dequeue, or implement delegates
- SwiftUI Views are extremely lightweight
- Unlike UIViews in UIKit, most SwiftUI Views exist as Swift structs and are created, passed, and referenced as value parameters
- structs avoids a plethora of memory allocations and the creation of many heavily subclassed and dynamic message-passing UIKit-based UIViews.
- the nodes in the view tree are monitored for state changes and usually don’t need to be rerendered if unchanged.
- Each and every UIView, on the other hand, is allocated and exists as a linked subview somewhere on the layout rendering tree and is an active part of the layout process.
- Still have some bugs and doesn't show error very well
- No way to control the navigation stack
- No swipe gestures for tableview cells
- View composition: create as many distinct and special purpose views as your app may require
- Use Semantic Color:
Color.primary,Color.secondary, andColor.accentColorare all just examples of colors provided by the system and environment. Colors adapt to light and dark mode, varying slightly in the process. - Use group: group things that need to be manipulated together or that share common attributes.
- Let the system do it’s thing: SwiftUI will properly adjust colors, spacing, padding, and the like based on the platform, on the current screen and/or container size, on control state. It will also take into consideration any changes needed for any accessibility features that might be enabled, do the right thing for Light/Dark mode, and more.
- Bind state as low in the hierarchy as possible
- Use EnvironmentValues