Swift UI List: Open external URL when cell is tapped - ios

I am trying to open a URL when one of the List's cells is tapped. I tried adding the modifier onTapGesture to the cell itself and then calling UIApplication.shared.open(url), but this only works if the tap is right on the cell view's elements (and not on the cell's background).
I also tried to add a background view (Rectangle) to the cell with opacity 0.01, but although this works the Rectangle is quite visible despite its low opacity.
Is there any workaround to make the whole row tappable?

Found a solution, here it is in case it helps anyone in the future:
// edit:
Used a single Button, with the required action (i.e. openURL in my case) in the action closure, and with my custom view returned in the label closure.

#stakri, What you're looking for is ".contentShape()"
Rectangle()
.stroke()
.onTapGesture() {
UIApplication.shared.open(URL(string: "https://stackoverflow.com")!)
}
above code will only work if you tap on the 'stroke' outline of the rectangle, but .contentShape() will make the entire area tappable without the need to nest it inside of a Stack:
Rectangle()
.stroke()
.contentShape( Rectangle() )
.onTapGesture() {
UIApplication.shared.open(URL(string: "https://stackoverflow.com")!)
}
Others coming to this question will likely be looking for how to specifically open a URL, you'll notice I force unwrapped my URL. Open to feedback on that, but barring any issues brought up, this works well.

Related

Disabling user interaction in a SwiftUI view without changing its appearance

I have a custom view InteractiveView that allows user interaction. I want to show a thumbnail of that view on an overview page (inside a NavigationLink) so the user can tap it to navigate to the the fullscreen view.
For that reason, I need the InteractiveView to be non-interactive (i.e. disabled) when it's displayed as a thumbnail. I implemented this as follows:
NavigationLink {
InteractiveView(viewModel)
} label: {
InteractiveView(viewModel)
.disabled(true)
}
This works as intended (i.e. tapping the view does not interact with the view but performs the navigation to the fullscreen interactive view instead).
However, the disabled(true) modifier also changes the InteractiveView's appearance: All its subviews are faded out, i.e. their opacity is reduced and they appear semi-transparent. I understand that this is usually what I want as it signals to the user that the view is disabled and I cannot interact with it. But in my case, the user can interact with it as they can tap on it in order to show the fullscreen view.
Question:
How can I disable the InteractiveView while keeping its original appearance (without the fade-out effect)?
Or: Is there a better way to disable all controls in a view without changing their appearance?
Update (Additional Information)
Many answers to this question suggest using hit testing with .allowsHitTesting(false) instead of .disabled(true) in the code above. This works indeed in terms of navigation, but it violates another requirement specified in the question: namely, the "without changing its appearance" part.
Why hit testing doesn't work
A NavigationLink always changes the foreground color of its label view to blue, thus it modifies the label view's appearance. I solved this problem by using a PlainButtonStyle on the NavigationLink:
NavigationLink {
InteractiveView(viewModel)
} label: {
InteractiveView(viewModel)
.allowsHitTesting(false)
}
.buttonStyle(PlainButtonStyle()) // prevent change of foreground color
With this button style, the navigation link breaks when I add the .allowsHitTesting(false) modifier: The navigation link's content view doesn't intercept touches anymore, but the navigation link itself also doesn't receive (or handle) those touches. And that's the problem:
I need the correct (normal) navigation link behavior without the typical navigation link highlighting.
Seems like allowsHitTesting is what you may be after.
https://stackoverflow.com/a/58912816/1066424
NavigationLink {
InteractiveView()
} label: {
InteractiveView()
.allowsHitTesting(false)
}

Get frame/dimensions from SwiftUI view(s)

Let's assume, to present a specific piece of information I have two different kinds of custom SwiftUI views. For the sake of an example my data is two Strings and the options to display them is either in a
HStack { Text() Spacer() Text() }
or
VStack {
Text()
Text()
}
style. In order to select the best fitting one, I would need to render them and make a choice based on resulting dimensions. For instance, if one of the text views in the first style would approach 50% of the window size, I would rather go with the second style.
How would I go about this without the user seeing the temporary views?
I know about GeometryReader, but I don't know how I could render my "candidate views" off screen, determine sizes and then make a selection for my actual view hierarchy.
Any hints?
I don't know how to work properly with SwiftUI. However, the following is an approach for what you want using UIKit and Labels for text:
let max_width = UIScreen.main.bounds.width/2
if label.intrinsicContentSize.width > max_width{
//The text would occupy more than 50% of the screen in width
}else{
//The text won't occupy more than 50% of the screen in width
}
You would have to run the previous function on each Label and there you can decide.
I know this is not a complete answer because you are working with SwiftUI, but someone may know how to apply the previous code on there.

SwiftUI LazyVGrid animating change of "page"

I’m trying to treat an entire LazyVGrid as pages, and animate when switching forward or back. A simple slide animation would be fine.
I currently have something along the lines of
LazyVGrid(
columns: [array of column info],
alignment: .center,
spacing: 4
) {
ForEach(cells) { cell in
CellView(cell: cell)
}
}
I have pages of cells that get set on the state when you hit the fwd or back buttons, and they are immediately re-rendered. However, it would be cool to set a forward/back slide when the buttons are hit. I haven’t been able to figure this out easily with transition and animation.
If there’s an entirely different library doing this, I’d be open to trying it.
Thanks so much!

How to prevent automatic scrolling of UICollectionView by VoiceOver?

I have a horizontally scrolled UICollectionView with a title label above it and a UIPageControl below it.
UILabel
UICollectionView
UIPageControl
When I turn on the VoiceOver accessibility feature and start traversing the screen sequentially, the collection view scrolls to the beginning or end automatically. Making the page jump off suddenly. For example, if I scroll to the 2nd page using page control, and move back to the collection view, it shows and reads the last page unexpectedly. Since I'm using the page control for navigation in the accessibility mode, I'd like to prevent the automatic scrolling.
How do I prevent or counter that?
I found an issue that seems to describe the same problem, but there's no workaround suggestion: iOS 8.4: Scroll view resets contentOffset with Voice Over enabled shortly after view appear
I encountered it on iOS 13.4.1 iPhone 11 Pro
UIScrollViewDelegate.scrollViewDidScroll(_:)
A change in the accessibility focus that triggers an automatic scrolling also triggers a call to scrollViewDidScroll(_:) in your UIScrollViewDelegate. Use that to counter the automatic scrolling effect, f.i. by setting contentOffset the way you prefer it.
You may need to detect that the scrolling was actually triggered by accessibility features, and not the user dragging or pinching. UIAccessibility.isVoiceOverRunning and UIAccessibilityFocus.accessibilityElementDidBecomeFocused() are your friends here. Beware that changing contentOffset (or zoomScale or whatever is needed) may trigger another call to scrollViewDidScroll(_:), so you need to prevent an infinite recursion.
Using #pommy's suggestions, I was able to fix my similar issue. In the code I was working on, the most appropriate place to make the change ended up being CalendarCollectionView.setContentOffset(_:animated:), where CalendarCollectionView is a UICollectionView subclass. Specifically, it's a JTACMonthView subclass, but that should not be of any relevance to this answer.
From the name, you can see my use case: a calendar which shows a month at a time. It could have many months both into the future and the past, but the usual user focus is likely to start somewhere in the middle.
Like the OP, I found that swiping from an outer element to the collection view with VoiceOver enabled caused the focus to go to the first date in the calendar, in my case 1st January 1951 (Date.farPast, I believe.) An interesting aside: Switch Control navigation did not cause the same behaviour.
The underlying behaviour was that contentOffset was getting set to 0.0 in the dimension that the collection view scrolls. In my code, that direction is held in style, and changes based on configuration, but in most applications it's likely to be fixed.
My code simply blocks any offset changes to 0.0 when VoiceOver is enabled. This is pretty naïve, and won't be suitable for all apps, but gives a concrete example which I hope will help some others!
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
if shouldPreventAccessibilityFocusScrollback(for: contentOffset) {
return
}
super.setContentOffset(contentOffset, animated: animated)
}
func shouldPreventAccessibilityFocusScrollback(for newContentOffset: CGPoint) -> Bool {
if UIAccessibility.isVoiceOverRunning {
switch style {
case .horizontal:
return newContentOffset.x == 0
case .vertical:
return newContentOffset.y == 0
}
}
return false
}
I spent quite a long time trying to determine when UIAccessibilityFocus moved from something outside the collection view, to something inside the collection view, which is ideally the only time we want to block these automatic scrolls. I was unsuccessful, but I think that was mostly due to subclassing a third party collection view (the calendar). There's definitely more merit to that approach, if you can get it to work... but it will require some careful management of state.

How to move a table content to the top of the screen?

I am still new to iOS and Swift and I am using Swift4 and Xcode9 to edit an iOS project which is made with a prior versions of Xcode and Swift (Xcode8 and Swift3). In the project there is a TableViewController used. So the problem is when I open the project in Xcode9 and run it, it shows some space on the top. I tried changing y values, but didn't work. My questions are,
How to solve the upper mentioned problem. (There is a question like this in Stack Overflow already. But it's not due to different Xcode version. But I even tried all the suggestions in the answers. But none of them worked for me.)
When there is a text label or something on the top of the table content, I can remove that space, from the top, but it goes to in between that label and the table added. And the label is unable to get a click action or something when it's moved to the top. How to solve that?
Any suggestion/ answer will be highly appreciated. Thank you!
You can try this
if #available(iOS 11.0, *) {
yourTableView.contentInsetAdjustmentBehavior = .never
}
Ok I'm going to attempt to answer your question but I wasn't totally positive since you didn't include any code or screenshots.
Firstly. If you mean that there seems to be a gap between where the tableView starts and where the first cell is displayed, this is correct. You can fix that by doing:
tableView.contentInset = .zero
this means that any content inside the tableView starts goes all the way to the edges.
Now for the label receiving touches. You want to look at user interaction enabled values on the storyboard:
Whenever you have views that stack on top of each other, the view UNDER will not receive touches if the view on top has user interaction enabled. If you want a touch to bleed through another view you either can
1) avoid the issue by not stacking views on top of each other (often unavoidable, like putting a label on a view and wanting the background view to do something)
2) turn off the user interaction on the view on top so the view on bottom gets the touch
tableView.setContentOffset(CGPoint.zero, animated: true)
this code will move

Resources