This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision | |||
|
swiftui [2020/04/14 11:10] august [References] |
swiftui [2020/04/14 11:19] (current) august [Customisation] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== Swift UI ====== | ||
| + | ===== References ===== | ||
| + | * [[https://stackoverflow.com/questions/56491881/move-textfield-up-when-thekeyboard-has-appeared-by-using-swiftui-ios|Move screen on Keyboard entry]] | ||
| + | * [[https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader|Accessing view geometry]] | ||
| + | * [[https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject|State, ObservedObject, EnvironmentObject]] | ||
| + | * [[https://developer.apple.com/design/human-interface-guidelines/ios/overview/themes/|iOS Design guidelines]] | ||
| + | * [[https://www.hackingwithswift.com/quick-start/swiftui/swiftui-tips-and-tricks|Tips and Tricks]] | ||
| + | * [[https://medium.com/@axelhodler/creating-a-search-bar-for-swiftui-e216fe8c8c7f|Search Bar]] | ||
| + | * [[https://stackoverflow.com/questions/56822195/how-do-i-use-userdefaults-with-swiftui|UserDefaults]] | ||
| + | |||
| + | ==== Customisation === | ||
| + | * [[https://www.youtube.com/watch?v=mCtohqZ6cYg|TabView]] | ||
| + | * [[https://github.com/smartvipere75/bottombar-swiftui|BottomBar animated]] | ||
| + | ===== API ===== | ||
| + | To enable networking add to ''plist.info'': ''App Transport Security Settings'' and inside of it ''Allow Arbitrary Loads'' | ||
| + | ===== Navigation View ===== | ||
| + | Dynamic list generation | ||
| + | <code bash> | ||
| + | NavigationView { | ||
| + | List { | ||
| + | ForEach(categories.keys.sorted(), id: \.self) { key in | ||
| + | Text(key) | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | ''NavigationLink'' to other views (''NavigationLink'' renders with environment colours, use ''.foregroundColor(.primary)'' on Text and ''.renderingMode(.original)'' on Image) | ||
| + | <code> | ||
| + | NavigationLink(destination: LandmarkDetail(landmark: landmark)) { | ||
| + | Object{} | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Navigation view title is applied to the child | ||
| + | <code swift> | ||
| + | NavigationView { | ||
| + | VStack { | ||
| + | Text("x") | ||
| + | } | ||
| + | .navigationBarTitle(Text("Title")) | ||
| + | .navigationBarTitle(Text(hike.name), displayMode: .inline) | ||
| + | .navigationBarItems(leading: EditButton(), trailing: Button("Add New Order") { | ||
| + | self.isPresented.toggle() | ||
| + | }) | ||
| + | } | ||
| + | </code> | ||
| + | ===== UI changes / state handling ===== | ||
| + | === State === | ||
| + | ''@State'' allows to read/write to value and bind to changes in its value. When the state value changes, the view invalidates its appearance and recomputes the body. | ||
| + | <code swift> | ||
| + | @State var showingProfile = false | ||
| + | self.showingProfile.toggle() | ||
| + | .sheet(isPresented: $showingProfile) {} | ||
| + | </code> | ||
| + | |||
| + | === Binding === | ||
| + | @Binding decorated variables allow to change the values parent @State/@ObservedObject variables | ||
| + | <code swift> | ||
| + | @Binding var currentPage: Int | ||
| + | </code> | ||
| + | |||
| + | === EnvironmentObject === | ||
| + | ''@EnvironmentObject'' An environment object invalidates the current view whenever the observable object changes. | ||
| + | <code swift> | ||
| + | @EnvironmentObject var userData: UserData | ||
| + | </code> | ||
| + | The parent object has to pass ''UserData'' as | ||
| + | <code swift> | ||
| + | childView.environmentObject(UserData()) | ||
| + | </code> | ||
| + | |||
| + | For Preview mode it's necessary to provide environmentObject to ''PreviewProvider'' | ||
| + | |||
| + | ===== Library objects ===== | ||
| + | View | ||
| + | <code swift> | ||
| + | SomeView | ||
| + | .listRowInsets(EdgeInsets()) // stretches to screen edges | ||
| + | .padding([.leading, .bottom]) | ||
| + | </code> | ||
| + | |||
| + | Text | ||
| + | <code swift> | ||
| + | Text(self.categoryName) | ||
| + | .font(.headline) | ||
| + | .padding(.leading, 15) | ||
| + | .padding(.top, 5) | ||
| + | .foregroundColor(.primary) | ||
| + | .lineLimit(2) | ||
| + | .fixedSize(horizontal: false, vertical: true) | ||
| + | .fixedSize() | ||
| + | .font(.system(size: 18)) | ||
| + | </code> | ||
| + | |||
| + | |||
| + | HStack | ||
| + | <code swift> | ||
| + | HStack(alignment: .lastTextBaseline) {} | ||
| + | |||
| + | HStack(alignment: .top, spacing: 0) {} | ||
| + | .frame(height: 185) | ||
| + | .padding(.top, UIApplication.shared.windows.first?.safeAreaInsets.top) | ||
| + | .contentShape(Rectangle()) // allows to tapGesture on white space (use with Spacer) | ||
| + | </code> | ||
| + | |||
| + | VStack | ||
| + | <code swift> | ||
| + | VStack(alignment: .leading) {} | ||
| + | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||
| + | .edgesIgnoringSafeArea(.all) | ||
| + | </code> | ||
| + | |||
| + | ZStack | ||
| + | <code swift> | ||
| + | ZStack { | ||
| + | Color(.label).opacity(0.05).edgesIgnoringSafeArea(.all) | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Image | ||
| + | <code swift> | ||
| + | image | ||
| + | .resizable() | ||
| + | .aspectRatio(contentMode: .fit) | ||
| + | .frame(width: 155, height: 155) | ||
| + | .cornerRadius(5) | ||
| + | .accessibility(label: Text("User Profile")) | ||
| + | .renderingMode(.original) | ||
| + | .clipShape(Circle()) | ||
| + | .scaledToFit() | ||
| + | </code> | ||
| + | |||
| + | Picker | ||
| + | <code swift> | ||
| + | Picker("", selection: self.$addCofeeOrderVM.size) { | ||
| + | Text("Small").tag("Small") | ||
| + | Text("Medium").tag("Medium") | ||
| + | Text("Large").tag("Large") | ||
| + | }.pickerStyle(SegmentedPickerStyle()) | ||
| + | </code> | ||
| + | |||
| + | ForEach | ||
| + | <code swift> | ||
| + | private func delete(at offsets: IndexSet) { | ||
| + | offsets.forEach { index in | ||
| + | let orderVM = self.orderListVM.orders[index] | ||
| + | self.orderListVM.deleteOrder(orderVM) | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | ForEach(self.orderListVM.orders, id:\.id) { order in | ||
| + | }.onDelete(perform: delete) | ||
| + | </code> | ||
| + | ===== Preview ===== | ||
| + | Add multiple phone models to preview | ||
| + | <code swift> | ||
| + | ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in | ||
| + | LandmarkList() | ||
| + | .previewDevice(PreviewDevice(rawValue: deviceName)) | ||
| + | .previewDisplayName(deviceName) | ||
| + | .colorScheme(.dark) | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | If the phone screen is not needed, more compact display method is | ||
| + | <code swift> | ||
| + | viewObject().previewLayout(.sizeThatFits) | ||
| + | </code> | ||
| + | |||
| + | @Binding variables can be simulated in the preview using ''.constant'' function. | ||
| + | <code swift> | ||
| + | //some view | ||
| + | @Binding var score: Int | ||
| + | </code> | ||
| + | <code swift> | ||
| + | struct FancyScoreView_Previews: PreviewProvider { | ||
| + | static var previews: some View { | ||
| + | FancyScoreView(score: .constant(0)) | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | ===== Transitions ===== | ||
| + | It's recommenced to define custom transitions by extending ''AnyTransition'' (''moveAndFade'' can be accessed using dot notation) | ||
| + | <code swift> | ||
| + | extension AnyTransition { | ||
| + | static var moveAndFade: AnyTransition { | ||
| + | AnyTransition.slide | ||
| + | } | ||
| + | }</code> | ||
| + | |||
| + | ===== Animations ===== | ||
| + | <code swift> | ||
| + | extension Animation { | ||
| + | static func ripple() -> Animation { | ||
| + | Animation.default | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | ===== Gestures ===== | ||
| + | Tap gesture | ||
| + | <code swift> | ||
| + | Card() | ||
| + | .gesture(TapGesture(count: 1) | ||
| + | .onEnded{ | ||
| + | print("Tapped") | ||
| + | }) | ||
| + | </code> | ||
| + | |||
| + | Drag gesture | ||
| + | <code swift> | ||
| + | @State private var cardDragState = CGSize.zero | ||
| + | |||
| + | Card() | ||
| + | .animation(.spring()) | ||
| + | .offset(y: self.cardDragState.height) | ||
| + | .gesture(DragGesture() | ||
| + | .onChanged{ value in | ||
| + | self.cardDragState = value.translation | ||
| + | } | ||
| + | .onEnded { vaule in | ||
| + | self.cardDragState = CGSize.zero | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Scale gesture | ||
| + | <code swift> | ||
| + | @State private var scale: CGFloat = 1.0 | ||
| + | |||
| + | Card() | ||
| + | .resizable() | ||
| + | .scaleEffect(self.scale) | ||
| + | .frame(width: 300, height: 300) | ||
| + | .gesture(MagnificationGesture() | ||
| + | .onChanged { value in | ||
| + | self.scale = value.magnitude | ||
| + | }) | ||
| + | </code> | ||
| + | |||
| + | Rotation gesture | ||
| + | <code swift> | ||
| + | @State private var cardRotateState: Double = 0 | ||
| + | |||
| + | Card() | ||
| + | .gesture(RotationGesture() | ||
| + | .onChanged { value in | ||
| + | self.cardRotateState = value.degrees | ||
| + | } | ||
| + | .onEnded{ value in | ||
| + | self.cardRotateState = 0 | ||
| + | }) | ||
| + | </code> | ||
| + | ===== Other ===== | ||
| + | Set frame for image before scaling | ||
| + | <code swift> | ||
| + | image | ||
| + | .frame(width:300, height:300) | ||
| + | .scaleEffect(1.0/3.0) | ||
| + | .frame(width: 100, height: 100) | ||
| + | </code> | ||
| + | |||
| + | |||
| + | Colors | ||
| + | <code bash> | ||
| + | TextField("Username", text: $username) | ||
| + | .background(Color(UIColor.systemGray5)) | ||
| + | </code> | ||
| + | |||
| + | * [[https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/|ColorCodes]] | ||
| + | * [[https://www.fivestars.blog/code/ios-dark-mode-how-to.html|Semantic Colors]] | ||
| + | |||
| + | |||
| + | ===== Snippets ===== | ||
| + | ===Plus button circled with dynamic colors=== | ||
| + | <code swift> | ||
| + | Button(action: { | ||
| + | | ||
| + | }){ | ||
| + | Image(systemName: "plus") | ||
| + | .resizable() | ||
| + | .frame(width:25, height: 25) | ||
| + | .foregroundColor(Color(.systemIndigo)) | ||
| + | .padding(25) | ||
| + | } | ||
| + | .background(Color(.blue).opacity(0.3)) | ||
| + | .clipShape(Circle()) | ||
| + | </code> | ||
| + | |||
| + | ===Custom border rounding=== | ||
| + | <code swift> | ||
| + | struct shape: Shape { | ||
| + | func path(in rect: CGRect) -> Path { | ||
| + | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 22, height: 22)) | ||
| + | | ||
| + | return Path(path.cgPath) | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | Can be applied to view or image | ||
| + | <code swift> | ||
| + | view.clipShape(shape()) | ||
| + | </code> | ||
| + | |||
| + | ===Timer publishing values=== | ||
| + | <code swift> | ||
| + | import SwiftUI | ||
| + | import Combine | ||
| + | |||
| + | class FancyTimer: ObservableObject { | ||
| + | | ||
| + | @Published var value: Int = 0 | ||
| + | | ||
| + | init() { | ||
| + | Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in | ||
| + | self.value += 1 | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | === ObservedObject, ObservableObject, Published === | ||
| + | <code swift> | ||
| + | import Foundation | ||
| + | import SwiftUI | ||
| + | import Combine | ||
| + | |||
| + | class UserSettings: ObservableObject { | ||
| + | | ||
| + | @Published var score: Int = 0 | ||
| + | | ||
| + | } | ||
| + | </code> | ||
| + | <code swift> | ||
| + | struct ContentView: View { | ||
| + | | ||
| + | @ObservedObject var userSettings = UserSettings() | ||
| + | | ||
| + | var body: some View { | ||
| + | VStack { | ||
| + | Text("\(userSettings.score)") | ||
| + | .font(.largeTitle) | ||
| + | | ||
| + | Button("Increment Score") { | ||
| + | self.userSettings.score += 1 | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | ===== xCode keyboard shortcuts ===== | ||
| + | * ''cmd+alt+['' - move line up | ||
| + | * ''cmd+alt+P'' - resume preview | ||
| + | * ''ctrl+I'' - indent selected | ||
| + | * ''cmd+B'' - build | ||
| + | * ''alt+cmb+P'' - resume preview | ||
| + | |||