Is there a SwiftUI equivalent to WKInterfaceController's .becomeCurrentPage method? - ios

Previously in WatchKit we could tell a certain InterfaceController to present itself using .becomeCurrentPage, how can we do it in Swift UI?
In WatchKit for example I would:
// handle notification
#objc func respondToWaterlock(_ notification: NSNotification) {
self.becomeCurrentPage()
}

there is no equivalent for becomeCurrentPage method in SwiftUI. You can update your State or ViewModel to achieve a similar result.
for example:
enum Pages {
case home
case settings
}
struct MyView: View {
#State var selectedPage: Pages = .home
var body: some View {
Group {
if self.selectedPage == .home {
Text("Home")
} else if self.selectedPage == .settings {
Text("Settings")
}
}
}
}
You should just update the selectedPage state variable to change the page.

Related

iOS 16 Binding issue (in this case Toggle)

What i have stumped across is that when i pass a Binding<> as a parameter in a ViewModel as you will see below, it doesn't always work as intended. It works in iOS 15 but not in iOS 16. This reduces the capability to create subviews to handle this without sending the entire Holder object as a #Binding down to it's subview, which i would say would be bad practice.
So i have tried to minimize this as much as possible and here is my leasy amount of code to reproduce it:
import SwiftUI
enum Loadable<T> {
case loaded(T)
case notRequested
}
struct SuperBinder {
let loadable: Binding<Loadable<ContentView.ViewModel>>
let toggler: Binding<Bool>
}
struct Holder {
var loadable: Loadable<ContentView.ViewModel> = .notRequested
var toggler: Bool = false
func generateContent(superBinder: SuperBinder) {
superBinder.loadable.wrappedValue = .loaded(.init(toggleBinder: superBinder.toggler))
}
}
struct ContentView: View {
#State var holder = Holder()
var superBinder: SuperBinder {
.init(loadable: $holder.loadable, toggler: $holder.toggler)
}
var body: some View {
switch holder.loadable {
case .notRequested:
Text("")
.onAppear(perform: {
holder.generateContent(superBinder: superBinder)
})
case .loaded(let viewModel):
Toggle("testA", isOn: viewModel.toggleBinder) // Works but doesn't update UI
Toggle("testB", isOn: $holder.toggler) // Works completly
// Pressing TestA will even toggle TestB
// TestB can be turned of directly but has to be pressed twice to turn on. Or something?
}
}
struct ViewModel {
let toggleBinder: Binding<Bool>
}
}
anyone else stumbled across the same problem? Or do i not understand how binders work?

Cell in List with LazyVGrid Disappears sometimes

I have this:
VStack {
List {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item, viewModel: viewModel, config: Self.config)
.onTapGesture {
switch viewModel.changeMode {
case .moving, .sharing, .moveAll:
viewModel.toggleItemToChange(item: item)
case .none:
object = item
viewModel.showCellDetails = true
}
}
.onLongPressGesture {
viewModel.restartDownload(fileGroupUUID: item.fileGroupUUID)
}
} // end ForEach
} // end LazyVGrid
}
.listStyle(PlainListStyle())
.refreshable {
viewModel.refresh()
}
.padding(5)
// Mostly this is to animate updates from the menu. E.g., the sorting order.
.animation(.easeInOut)
// Had a problem with return animation for a while: https://stackoverflow.com/questions/65101561
// The solution was to take the NavigationLink out of the scrollview/LazyVGrid above.
if let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
ObjectDetailsView(object: object, model: ObjectDetailsModel(object: object)),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
} // end if
} // end VStack
AlbumItemsScreenCell:
struct AlbumItemsScreenCell: View {
#StateObject var object:ServerObjectModel
#StateObject var viewModel:AlbumItemsViewModel
let config: IconConfig
#Environment(\.colorScheme) var colorScheme
var body: some View {
AnyIcon(model: AnyIconModel(object: object), config: config,
emptyUpperRightView: viewModel.changeMode == .none,
upperRightView: {
UpperRightChangeIcon(object: object, viewModel: viewModel)
})
}
}
When a user taps one of the cells, this causes navigation to a details screen. Sometimes when the user returns from that navigation, the cell in the upper left disappears:
https://www.dropbox.com/s/mi6j2ie7h8dcdm0/disappearingCell.mp4?dl=0
My current hypothesis about the issue is that when user actions in that details screen take actions which change viewModel.objects, this causes the disappearing cell problem. I'll be testing this hypothesis shortly.
----- Update, 11/1/21 ------
Well, that hypothesis was wrong. I now understand the structure of the problem more clearly. Still don't have a fix though.
Tapping on one of the AlbumItemsScreenCells navigates to a details screen (I've added to the code above to show that). In the details screen user actions can cause a comment count to get reset, which sends a Notification.
A model in the AlbumItemsScreenCell listens for these notification (for the specific cell) and resets a badge on the cell.
Here is that model:
class AnyIconModel: ObservableObject, CommentCountsObserverDelegate, MediaItemBadgeObserverDelegate, NewItemBadgeObserverDelegate {
#Published var mediaItemBadge: MediaItemBadge?
#Published var unreadCountBadgeText: String?
#Published var newItem: Bool = false
var mediaItemCommentCount:CommentCountsObserver!
let object: ServerObjectModel
var mediaItemBadgeObserver: MediaItemBadgeObserver!
var newItemObserver: NewItemBadgeObserver!
init(object: ServerObjectModel) {
self.object = object
// This is causing https://stackoverflow.com/questions/69783232/cell-in-list-with-lazyvgrid-disappears-sometimes
mediaItemCommentCount = CommentCountsObserver(object: object, delegate: self)
mediaItemBadgeObserver = MediaItemBadgeObserver(object: object, delegate: self)
newItemObserver = NewItemBadgeObserver(object: object, delegate: self)
}
}
The unreadCountBadgeText gets changed (on the main thread) by the observer when the Notification is received.
So, in summary, the badge on the cell gets changed while the screen with the cells is not displayed-- the details screen is displayed.
I had been using the following conditional modifier:
extension View {
public func enabled(_ enabled: Bool) -> some View {
return self.disabled(!enabled)
}
// https://forums.swift.org/t/conditionally-apply-modifier-in-swiftui/32815/16
#ViewBuilder func `if`<T>(_ condition: Bool, transform: (Self) -> T) -> some View where T : View {
if condition {
transform(self)
} else {
self
}
}
}
to display the badge on the AlbumItemsScreenCell.
The original badge looked like this:
extension View {
func upperLeftBadge(_ badgeText: String) -> some View {
return self.modifier(UpperLeftBadge(badgeText))
}
}
struct UpperLeftBadge: ViewModifier {
let badgeText: String
init(_ badgeText: String) {
self.badgeText = badgeText
}
func body(content: Content) -> some View {
content
.overlay(
ZStack {
Badge(badgeText)
}
.padding([.top, .leading], 5),
alignment: .topLeading
)
}
}
i.e., the usage looked like this in the cell:
.if(condition) {
$0.upperLeftBadge(badgeText)
}
when I changed the modifier to use it with out this .if modifier, and used it directly, the issue went away:
extension View {
func upperLeftBadge(_ badgeText: String?) -> some View {
return self.modifier(UpperLeftBadge(badgeText: badgeText))
}
}
struct UpperLeftBadge: ViewModifier {
let badgeText: String?
func body(content: Content) -> some View {
content
.overlay(
ZStack {
if let badgeText = badgeText {
Badge(badgeText)
}
}
.padding([.top, .leading], 5),
alignment: .topLeading
)
}
}
It may sounds weird, but in my tvOS app i disable the animations
and cells(Views) stops to disappear
So try to remove .animation(.easeInOut)

SwiftUI - How can I call a Function in my View?

I am using SwiftUI/Swift for a week now, and I love it. Now I have a problem. I want to call a Function from my View, but I get this Error
Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
This is the Code:
struct testView: View {
var body: some View {
VStack {
Text("TextBox")
Text("SecondTextBox")
self.testFunction()
}
}
func testFunction() {
print("This is a Text.")
}
}
I don't get it. In other languages its much simpler and could work that way. Can anybody help me please? Swift is pretty new to me :D
Meanwhile here are the places (not all) where/how you can call a function
init() {
self.testFunction() // non always good, but can
}
var body: some View {
self.testFunction() // 1)
return VStack {
Text("TextBox")
.onTapGesture {
self.testFunction() // 2)
}
Text("SecondTextBox")
}
.onAppear {
self.testFunction() // 3)
}
.onDisappear {
self.testFunction() // 4)
}
}
... and so on
An additional method:
Testing with Swift 5.8, you can also stick in a let _ = self.testFunction().
eg
(this is extra-contrived so that it's possible to see the effect in Preview, because print() doesn't happen in Preview, at least for me)
import SwiftUI
class MyClass {
var counter = 0
}
struct ContentView: View {
var myClass: MyClass
var body: some View {
VStack {
Text("TextBox counter = \(myClass.counter)")
// v--------------------------------------------
//
let _ = self.testFunction() // compiles happily
// self.testFunction() // does not compile
//
// ^--------------------------------------------
Text("TextBox counter = \(myClass.counter)")
}
}
func testFunction() {
print("This is a Test.")
myClass.counter += 1
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(myClass: MyClass())
}
}
Using Swift 5.3 and Xcode 12.4
I use a little extension to debug inside the Views (VStack in the example), e.g. to inspect a geometryReader.size. It could be used to call any function in the View as follows:
NOTE: For debugging purposes only. I don't recommend including code like this in any production code
VStack {
Text.console("Hello, World")
Text("TEXT"
}
extension Text {
static func console<T>(_ value: T) -> EmptyView {
print("\(value)")
return EmptyView()
}
}
This will print "Hello, World" to the console.....
There is a reason, when writing in swift UI (iOS Apps), that there is a View protocol that needs to be followed. Calling functions from inside the structure is not compliant with the protocol.
The best thing you can do is the .on(insertTimeORAction here).
Read more about it here

Swift UI need to keep both NavigationLink to detail view and Tap gesture recognizer

I am trying a simple app that is a List with items, they lead to detail view. I also have a search bar that opens keyboard, and I need to hide the keyboard when the user taps anywhere outside of the keyboard.
#State private var keyboardOpen: Bool = false
var body: some View {
NavigationView {
Form {
Section {
TextField("Search", text: $cityStore.searchTerm, onCommit: debouncedFetch)
.keyboardType(.namePhonePad)
.disableAutocorrection(true)
.onTapGesture { self.keyboardOpen = true }
.onDisappear { self.keyboardOpen = false }
}
Section {
List {
ForEach(cities) { city in
NavigationLink(
destination: DetailView(city: city)) {
VStack(alignment: .leading) {
Text("\(city.name)")
}
}
}
}
}
}
.navigationBarTitle("City list")
.onTapGesture {
if self.keyboardOpen {
UIApplication.shared.endEditing()
self.keyboardOpen = false
}
}
}
}
Do you know if it's possible to keep both gesture tap and follow to detail view?
Actually it should work, but it is not due to bug of .all GestureMask. I submitted feedback to Apple #FB7672055, and recommend to do the same for everybody affected, the more the better.
Meanwhile, here is possible alternate approach/workaround to achieve similar effect.
Tested with Xcode 11.4 / iOS 13.4
extension UIApplication { // just helper extension
static func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
}
}
struct TestEndEditingOnNavigate: View {
#State private var cities = ["London", "Berlin", "New York"]
#State private var searchTerm = ""
#State private var tappedLink: String? = nil // track tapped link
var body: some View {
NavigationView {
Form {
Section {
TextField("Search", text: $searchTerm)
}
Section {
List {
ForEach(cities, id: \.self) { city in
self.link(for: city) // decompose for simplicity
}
}
}
}
.navigationBarTitle("City list")
}
}
private func link(for city: String) -> some View {
let selection = Binding(get: { self.tappedLink }, // proxy bindng to inject...
set: {
UIApplication.endEditing() // ... side effect on willSet
self.tappedLink = $0
})
return NavigationLink(destination: Text("city: \(city)"), tag: city, selection: selection) {
Text("\(city)")
}
}
}
I think you could easily handle this scenario with boolean flags, when your keyboard opens you can set a flag as true and when it dismisses a the flag goes back to false, so in that case when the keyboard is open and the tap gesture is triggered you can check if the keyboard flag is active and not go to detail but instead effectively dismiss the keyboard and viceversa. Let me know if maybe I misunderstood you.

SwiftUI : Dismiss modal from child view

I'm attempting to dismiss a modal after its intended action is completed, but I have no idea how this can be currently done in SwiftUI. This modal is triggered by a #State value change. Would it be possible to change this value by observing a notification of sorts?
Desired actions: Root -> Initial Modal -> Presents Children -> Dismiss modal from any child
Below is what I've tried
Error: Escaping closure captures mutating 'self' parameter
struct AContentView: View {
#State var pageSaveInProgress: Bool = false
init(pages: [Page] = []) {
// Observe change to notify of completed action
NotificationCenter.default.publisher(for: .didCompletePageSave).sink { (pageSaveInProgress) in
self.pageSaveInProgress = false
}
}
var body: some View {
VStack {
//ETC
.sheet(isPresented: $pageSaveInProgress) {
ModalWithChildren()
}
}
}
}
ModalWithChildren test action
Button(action: {
NotificationCenter.default.post(
name: .didCompletePageSave, object: nil)},
label: { Text("Close") })
You can receive messages through .onReceive(_:perform) which can be called on any view. It registers a sink and saves the cancellable inside the view which makes the subscriber live as long as the view itself does.
Through it you can initiate #State attribute changes since it starts from the view body. Otherwise you would have to use an ObservableObject to which change can be initiated from anywhere.
An example:
struct MyView : View {
#State private var currentStatusValue = "ok"
var body: some View {
Text("Current status: \(currentStatusValue)")
}
.onReceive(MyPublisher.currentStatusPublisher) { newStatus in
self.currentStatusValue = newStatus
}
}
A complete example
import SwiftUI
import Combine
extension Notification.Name {
static var didCompletePageSave: Notification.Name {
return Notification.Name("did complete page save")
}
}
struct OnReceiveView: View {
#State var pageSaveInProgress: Bool = true
var body: some View {
VStack {
Text("Usual")
.onReceive(NotificationCenter.default.publisher(for: .didCompletePageSave)) {_ in
self.pageSaveInProgress = false
}
.sheet(isPresented: $pageSaveInProgress) {
ModalWithChildren()
}
}
}
}
struct ModalWithChildren: View {
#State var presentChildModals: Bool = false
var body: some View {
Button(action: {
NotificationCenter.default.post(
name: .didCompletePageSave,
object: nil
)
}) { Text("Send message") }
}
}

Resources