How to map Binding with optional value in SwiftUI

Issue #838 We can write our custom Binding import SwiftUI extension Binding where Value == Date? { func flatten(defaultValue: Date) -> Binding<Date> { Binding<Date>( get: { wrappedValue ?? defaultValue }, set: { wrappedValue = $0 } ) } } Then use in places where it needs Binding with non-optional value DatePicker( "", selection: $date.flatten(defaultValue: Date()), displayedComponents: .hourAndMinute ) .datePickerStyle(WheelDatePickerStyle())

August 10, 2021 路 1 min 路 Khoa Pham

How to get view height in SwiftUI

Issue #837 I usually use GeometryReader in background to get size of view, and encapsulate it inside a ViewModifier struct GetHeightModifier: ViewModifier { @Binding var height: CGFloat func body(content: Content) -> some View { content.background( GeometryReader { geo -> Color in DispatchQueue.main.async { height = geo.size.height } return Color.clear } ) } } This is useful to constrain NavigationView height when in use inside a bottom sheet struct FilterView: View { @State private var height: CGFloat = 0 var body: some View { BottomSheet { NavigationView { VStack { Text("content") } ....

August 6, 2021 路 1 min 路 Khoa Pham

How to prevent wheel Picker conflict with DragGesture in SwiftUI

Issue #836 If we have a Picker inside a View that has DragGesture, chances are when we scroll the wheel, the DragGesture is detected too To prevent this, we can attach a dummy DragGesture to our Picker Picker("", selection: $number) { ForEach(FavoriteNumbers.allCases) { number in Text( } } .pickerStyle(WheelPickerStyle()) .gesture(DragGesture())

August 5, 2021 路 1 min 路 Khoa Pham

How to make equal width buttons in SwiftUI

Issue #835 I usually define ButtonStyle to encapsulate common styles related to buttons. Here we specify .frame(maxWidth: .infinity) to let this button take the whole width as possible struct MyActionButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(.headline.bold()) .foregroundColor(.white) .frame(height: 44) .frame(maxWidth: .infinity) .background( .cornerRadius(8) } } If we have 2 buttons both specifying maxWidth: .infinity then they will be divided equally with a spacing specified by our HStack...

August 5, 2021 路 1 min 路 Khoa Pham

How to make bottom sheet in SwiftUI

Issue #834 In iOS 15, we can use UISheetPresentationController to show bottom sheet like native Maps app. But before that there鈥檚 no such built in bottom sheet in UIKit or SwiftUI. We can start defining API for it. There are 3 ways to show overlay content in SwiftUI fullScreenCover: this presents like a modal view controller bottom up offset: initially hide our bottom sheet overlay: show overlay full screen so we can have custom control....

August 5, 2021 路 2 min 路 Khoa Pham

How to show abbreviated ago Date in Swift

Issue #833 Use RelativeDateTimeFormatter. This assumes US locale is used extension Date { private static let relativeFormatter: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.calendar?.locale = Locale(identifier: "en_US_POSIX") formatter.dateTimeStyle = .numeric formatter.unitsStyle = .abbreviated return formatter }() var ago: String { guard abs(timeIntervalSinceNow) > 60 else { return "Just Now" } let text = Self.relativeFormatter.localizedString(for: self, relativeTo: Date()) return text .replacingOccurrences(of: "ago", with: "") .replacingOccurrences(of: "min", with: "m") ....

August 4, 2021 路 1 min 路 Khoa Pham

Icon won't take on final exe

Issue #832

August 4, 2021 路 1 min 路 Khoa Pham

How to bind to nested ObservableObject in SwiftUI

Issue #831 We can normally listen to sub ObservableObject by either listen to its objectWillChange or its Publisher class SubModel: ObservableObject { @Published var count = 0 } class AppModel: ObservableObject { @Published var submodel: SubModel = SubModel() private var bag = Set<AnyCancellable>() init() { submodel.$count .sink(receiveValue: { [weak self] _ in self?.objectWillChange.send() }) } } We can then make a convenient extension to ease this. We use DispatchQueue to trigger objectWillChange in another run loop, to achieve didSet behavior...

July 27, 2021 路 1 min 路 Khoa Pham

How to structure navigation in SwiftUI apps

Issue #830 Read more Router Coordinator Coordinator

July 26, 2021 路 1 min 路 Khoa Pham

How to track contentSize and scroll offset for ScrollView in SwiftUI

Issue #829 Use PreferenceKey with a custom coordinateSpace to make our own TrackableScrollView. Note that the offset here is in reversed to contentOffset in normal UIScrollView import SwiftUI struct TrackableScrollView<Content: View>: View { @ViewBuilder let content: (ScrollViewProxy) -> Content let onContentSizeChange: (CGSize) -> Void let onOffsetChange: (CGPoint) -> Void var body: some View { ScrollViewReader { reader in ScrollView(showsIndicators: false) { GeometryReader { geo in Color.clear.preference( key: ScrollOffsetKey.self, value: geo.frame(in: ....

July 25, 2021 路 1 min 路 Khoa Pham

How to focus TextField in SwiftUI

Issue #828 From iOS 15, FocusState Use focused(_:) for single TextField, and focused(_:equals:) for multiple TextField struct FormView: View { @FocusState private var isFocused: Bool @State private var name = "" var body: some View { TextField("Name", text: $name) .focused($isFocused) } } struct FormView: View { enum Field: Hashable { case usernameField case passwordField } @State private var username = "" @State private var password = "" @FocusState private var focusedField: Field?...

July 23, 2021 路 1 min 路 Khoa Pham

How to use debounce in Combine in Swift

Issue #827 I鈥檓 trying a simple Future and sink. As long as I have debounce, only receiveCompletion gets called, but not receiveValue private var bag = Set<AnyCancellable>() let future = Future<Int, Never> { promise in promise(.success(1)) } future .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) .sink( receiveCompletion: { print($0) // Called }, receiveValue: { print($0) // Not called } ) .store(in: &bag) Reading debounce Publishes elements only after a specified time interval elapses between events....

July 21, 2021 路 1 min 路 Khoa Pham

How to use Button inside NavigationLink in SwiftUI

Issue #826 Use isActive binding @State private var goesToDetail: Bool = false NavigationLink( destination: DetailView(viewModel: viewModel), isActive: $goesToDetail) { Button(action: { goesToDetail = true }) { Text("Next") } .buttonStyle(MyButtonStyle)) }

July 21, 2021 路 1 min 路 Khoa Pham

How to structure user state for App in SwiftUI

Issue #825 For many apps that require user authentication, a common practice is to define a shared UserManager with an optional User. This is convenient but it requires us to constantly unwrap and check that user class UserManager { static let shared = UserManager() private(set) var user: User? } A more safer approach is to leverage Swift type system and separate the need based on authenticated and unauthenticated usage. For example, for an unauthorized users, we show the login screen....

July 21, 2021 路 2 min 路 Khoa Pham

How to do NavigationLink programatically in SwiftUI

Issue #824 Use a custom NavigationLink with EmptyView as the background, this failable initializer accepts Binding of optional value. This works well as the destination are made lazily. extension NavigationLink where Label == EmptyView { init?<Value>( _ binding: Binding<Value?>, @ViewBuilder destination: (Value) -> Destination ) { guard let value = binding.wrappedValue else { return nil } let isActive = Binding( get: { true }, set: { newValue in if !newValue { binding....

July 20, 2021 路 1 min 路 Khoa Pham

How to make custom navigation bar in SwiftUI

Issue #823 Make atomic components and compose them. Firstly with NavigationBar that has custom leading and trailing content, there we can customize padding. import SwiftUI struct NavigationBar<Leading: View, Trailing: View>: View { @ViewBuilder let leading: Leading @ViewBuilder let trailing: Trailing var body: some View { HStack { leading Spacer() trailing } .padding(.horizontal, 8) } } struct NavigationBarWithBackButton: View { let onBack: () -> Void var body: some View { NavigationBar( leading: backButton, trailing: EmptyView....

July 20, 2021 路 1 min 路 Khoa Pham

How to convert NSEvent locationInWindow to window coordinate

Issue #822 Get active screen with mouse func activeScreen() -> NSScreen? { let mouseLocation = NSEvent.mouseLocation let screens = NSScreen.screens let screenWithMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }) return screenWithMouse } Reposition our NSWIndow if window.frame != screen.frame { window.setFrameOrigin(screen.frame.origin) window.setContentSize(screen.frame.size) } Flip y as AppKit has coordinate origin starting from bottom left of the screen, while our SwiftUI starts from top left var point = window.convertPoint(fromScreen: event.locationInWindow) point.y = window....

July 13, 2021 路 1 min 路 Khoa Pham

How to sync width for child views in SwiftUI

Issue #821 Suppose we have a 16:9 preview Image and we want the title Text to be the same width First we need PreferenceKey to store and sync size struct SizePreferenceKey: PreferenceKey { typealias Value = CGSize static var defaultValue: Value = .zero static func reduce(value: inout Value, nextValue: () -> Value) { value = nextValue() } } For our Image, we use a GeometryReader in the background with invisible View Color....

July 6, 2021 路 1 min 路 Khoa Pham

How to highlight link in Text in SwiftUI

Issue #820 Use NSDataDetector to detect links, and NSMutableAttributedString to mark link range. Then we enumerateAttributes and build our Text func attribute(string: String) -> Text { guard let detector = try? NSDataDetector(types: else { return Text(string) } let stringRange = NSRange(location: 0, length: string.count) let matches = detector.matches( in: string, options: [], range: stringRange ) let attributedString = NSMutableAttributedString(string: string) for match in matches { attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single, range: match....

July 6, 2021 路 1 min 路 Khoa Pham

How to conform UIImage to Codable

Issue #819 Either use EasyStash or make a simple CodableImage struct CodableImage: Codable { let image: UIImage? init(image: UIImage) { self.image = image } enum CodingKeys: CodingKey { case data case scale } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let scale = try container.decode(CGFloat.self, forKey: .scale) let data = try container.decode(Data.self, forKey: .data) self.image = UIImage(data: data, scale: scale) } public func encode(to encoder: Encoder) throws { var container = encoder....

July 4, 2021 路 1 min 路 Khoa Pham