I am trying to add a PKCanvasView to a scrollview with SwiftUI.
But when I try to draw in the PKCanvasView it scrolls the scrollview instead.
In other words, how could I deactivate the scrolling when the user interacts with a specific view inside the scrollview?
I parsed through a lot of examples and blog articles and almost similar things but they were either too old or not for SwiftUI...
EDIT
To give further details (and based on Guillermo answer), here is an additional sample:
struct ContentView: View {
#State private var scrollDisabled = false
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill((i & 1 == 0) ? .blue: .yellow)
.frame(width: 300, height: 50)
.overlay {
Text("\(i)")
}
}
}.frame(maxWidth: .infinity)
}
.scrollDisabled(scrollDisabled)
}
.frame(width: 300)
}
}
Note that you cannot scroll touching the leading/trailing white borders.
I'd like the same behavior for yellow items: if you try to move up/down while touching a blue cell it scrolls, but if you try the same with yellow the scrollview shouldn't scroll!
Sorry, I'm really trying my best to be clear... ^^
You can control how your elements respond to the drag gesture with the following:
.gesture(DragGesture(minimumDistance:))
For instance:
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill((i & 1 == 0) ? .blue: .yellow)
.frame(width: 300, height: 50)
.overlay {
Text("\(i)")
}
.gesture(DragGesture(minimumDistance: i & 1 == 0 ? 100 : 0))
}
}.frame(maxWidth: .infinity)
}
}
.frame(width: 300)
}
}
will block the scrolling for the yellow elements!
In iOS 16 was added a modifier scrollDisabled to achieve what you need here's an example:
struct Scroll: View {
#State private var scrollDisabled = false
var body: some View {
VStack {
Button("\(scrollDisabled ? "Enable" : "Disable") Scroll") {
scrollDisabled.toggle()
}
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill(.blue)
.frame(width: 50, height: 50)
.overlay {
Text("\(i)")
}
}
}.frame(maxWidth: .infinity)
}
.scrollDisabled(scrollDisabled)
}
}
}
Related
I am trying to create two horizontal scrollable list of elements. Currently, this is what I am able to achieve:
As can be seen, I want to remove the space between the two Scroll Views. Here, is the code I am using to achieve this:
var body: some View {
NavigationView {
VStack (spacing: 0){
Text("Zara") .font(.custom(
"AmericanTypewriter",
fixedSize: 36))
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}.task {
await loadData()
}
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}
}
}
}
HorizontalScrollModel() returns a LazyHGrid of the products. I have tried setting the spacing to 0 with VStack (spacing: 0) but this did not work. How do I remove the space between the ScrollViews?
HorizontalScrollModel:
struct HorizontalScrollModel: View {
var searchResults1: [homeResult]
let columns = [
GridItem(.adaptive(minimum: 200))]
#State private var isHearted = false
init( searchResult: [homeResult]) {
self.searchResults1 = searchResult
}
var body: some View {
LazyHGrid (rows: columns ){
ForEach(searchResults1, id: \.prodlink) { item in
NavigationLink{
ProductView(imageLink: item.image_src, productLink: item.prodlink ,
productCost: String(item.price))
} label: {
VStack (alignment: .leading, spacing: 0){
AsyncImage(url: URL(string: item.image_src)) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 10))
} else if phase.error != nil {
Text("There was an error loading the image.")
} else {
ProgressView()
}
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.gray.opacity(0.2), lineWidth: 4)
)
.padding(.top, 2)
.padding(.bottom, 2)
.padding(.leading, 2)
.padding(.trailing, 2)
.overlay (
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.3), lineWidth: 1))
}
}
}
// .padding([.horizontal, .bottom])
}
}
}
Each ScrollView takes all available space offered by VStack, which since it has 2 views will be half the screen for each view.
Inside each ScrollView you then place a LazyHGrid, and inside the cell you are placing a 150x150 image, but the ScrollView size will not change to fit the image.
So, it now depends on what you're aiming for. If you just want the two ScrollView to be joined together, you can have a .frame modified to the outer VStack with a height of 308 (150 for each image + the padding).
Try adding the .fixedSize() modifier to the ScrollView:
ScrollView(.horizontal) {
HorizontalScrollModel(searchResult: searchResults1)
}
.fixedSize(horizontal: false, vertical: true)
You might also be able to add it just to the HorizontalScrollModel.
I am developing an App that supports multiple Profiles. I really like the way Apple displays the Profile Icon next to the Large Navigation Bar Title in all their Apps. See the Screenshot below:
My Question is the following:
Is it possible to achieve this in SwiftUI? And if so, how?
If it's not possible in pure SwiftUI, how can I achieve it including UIKit Code?
Thanks for your help.
I solved this by using SwiftUI-Introspect, to "Introspect underlying UIKit components from SwiftUI".
Here is an example of a view:
struct ContentView: View {
#State private var lastHostingView: UIView!
var body: some View {
NavigationView {
ScrollView {
ForEach(1 ... 50, id: \.self) { index in
Text("Index: \(index)")
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Large title")
.introspectNavigationController { navController in
let bar = navController.navigationBar
let hosting = UIHostingController(rootView: BarContent())
guard let hostingView = hosting.view else { return }
// bar.addSubview(hostingView) // <--- OPTION 1
// bar.subviews.first(where: \.clipsToBounds)?.addSubview(hostingView) // <--- OPTION 2
hostingView.backgroundColor = .clear
lastHostingView?.removeFromSuperview()
lastHostingView = hostingView
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8)
])
}
}
}
}
Bar content & profile picture views:
struct BarContent: View {
var body: some View {
Button {
print("Profile tapped")
} label: {
ProfilePicture()
}
}
}
struct ProfilePicture: View {
var body: some View {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.padding(.horizontal)
}
}
The .frame(width: 40, height: 40) & hostingView.bottomAnchor constant will need to be adjusted to your needs.
And the results for each option (commented in the code):
Option 1
Option 2
View sticks when scrolled
View disappearing underneath on scroll
Without NavigationView
I done this with pure SwiftUI. You have to replace the Image("Profile") line with your own image (maybe from Assets or from base64 data with UIImage).
HStack {
Text("Apps")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
.padding(.all, 30)
This products following result:
With NavigationView
Let's assume that you have NavigationView and inside that there's only ScrollView and .navigationTitle. You can add that profile image there by using overlay.
NavigationView {
ScrollView {
//your content here
}
.overlay(
ProfileView()
.padding(.trailing, 20)
.offset(x: 0, y: -50)
, alignment: .topTrailing)
.navigationTitle(Text("Apps"))
}
Where ProfileView could be something like this:
struct ProfileView: View {
var body: some View {
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
The result will be like this...
...which is pretty close to the App Store:
I'm making a simple task app and using ForEach to populate task rows with the task information from my model. I need a way to animate my task view to open up and reveal some description text and two buttons. I want to turn from A into B on tap, and then back again on tap:
Design Image
I've tried a couple things. I successfully got a proof-of-concept rectangle animating in a test project, but there are issues. The rectangle shrinks and grows from the centre point, vs. from the bottom only. When I place text inside it, the text doesn't get hidden and it looks really bad.
struct ContentView: View {
#State var animate = false
var animation: Animation = .spring()
var body: some View {
VStack {
Rectangle()
.frame(width: 200, height: animate ? 60 : 300)
.foregroundColor(.blue)
.onTapGesture {
withAnimation(animation) {
animate.toggle()
}
}
}
}
In my main app, I was able to replace my first task view (closed) with another view that's open. This works but it looks bad and it's not really doing what I want. It's effectively replacing the view with another one using a fade animation.
ForEach(taskArrayHigh) { task in
if animate == false {
TaskView(taskTitle: task.title, category: task.category?.rawValue ?? "", complete: task.complete?.rawValue ?? "", priorityColor: Color("HighPriority"), task: task, activeDate: activeDate)
.padding(.top, 10)
.padding(.horizontal)
.onTapGesture {
withAnimation(.easeIn) {
animate.toggle()
}
}
.transition(.move(edge: .bottom))
} else if animate == true {
TaskViewOpen(task: "Grocery Shopping", category: "Home", remaining: 204, completed: 4)
.padding(.top, 10)
.padding(.horizontal)
.onTapGesture {
withAnimation(.easeIn) {
animate.toggle()
}
}
}
Is there a way to animate my original closed view to open up and reveal the description text and buttons?
You are on the right track with your .transition line you have, but you want to make sure that the container stays the same and the contents change -- right now, you're replacing the entire view.
Here's a simple example illustrating the concept:
struct ContentView: View {
#State var isExpanded = false
var body: some View {
VStack {
Text("Headline")
if isExpanded {
Text("More Info")
Text("And more")
}
}
.padding()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.background(Color.gray.cornerRadius(10.0))
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
Since you're using it inside a ForEach, you'll probably want to abstract this into its own component, as it'll need its own #State to keep track of the expanded state as I've shown here.
Update, based on comments:
Example of using a PreferenceKey to get the height of the expandable view so that the frame can be animated and nothing fades in and out:
struct ContentView: View {
#State var isExpanded = false
#State var subviewHeight : CGFloat = 0
var body: some View {
VStack {
Text("Headline")
VStack {
Text("More Info")
Text("And more")
Text("And more")
Text("And more")
Text("And more")
Text("And more")
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
.onPreferenceChange(ViewHeightKey.self) { subviewHeight = $0 }
.frame(height: isExpanded ? subviewHeight : 50, alignment: .top)
.padding()
.clipped()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.background(Color.gray.cornerRadius(10.0))
.onTapGesture {
withAnimation(.easeIn(duration: 2.0)) {
isExpanded.toggle()
}
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
Using Swift 5 you can use withAnimation and have the view hidden based on state.
ExpandViewer
Has a button to show and hide the inner view
Takes in a content view
struct ExpandViewer <Content: View>: View {
#State private var isExpanded = false
#ViewBuilder let expandableView : Content
var body: some View {
VStack {
Button(action: {
withAnimation(.easeIn(duration: 0.5)) {
self.isExpanded.toggle()
}
}){
Text(self.isExpanded ? "Hide" : "View")
.foregroundColor(.white)
.frame(maxWidth: .infinity, minHeight: 40, alignment: .center)
.background(.blue)
.cornerRadius(5.0)
}
if self.isExpanded {
self.expandableView
}
}
}
}
Using the viewer
ExpandViewer {
Text("Hidden Text")
Text("Hidden Text")
}
I'm trying to animate an image inside a "row" by it's y axis so that it appears that you would the view would slowly scroll vertically through the entire image. And when done, it backtracks. I hope that makes sense. I'm trying to do it in this code:
var body: some View {
ZStack {
HStack {
GeometryReader { geometry in
WebImage(url: self.url)
.renderingMode(.original)
.resizable()
.placeholder {
ImageStore.shared.image(name: "exploras-icon")
}
.aspectRatio(contentMode: .fill)
.animate {
// animate by y offset within bounds of HStack
}
}
}
.frame(height: 140)
.clipped()
}
}
Any help/guidance much appreciated!
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4
struct TestAutoScrollImage: View {
#State private var isActive = false
var body: some View {
GeometryReader { gp in
HStack {
Image("large_image")
}
.frame(width: gp.size.width, height: gp.size.height, alignment: self.isActive ? .bottom : .top)
}
.edgesIgnoringSafeArea([.top, .bottom])
.animation(Animation.linear(duration: 5).repeatForever())
.onAppear {
self.isActive = true
}
}
}
I can wrap all my views inside a List
List {
// contents
}
But this seems to be vertical scrolling. How do I make it horizontal?
You need to add .horizontal property to the scrollview. otherwise it won't scroll.
ScrollView (.horizontal, showsIndicators: false) {
HStack {
//contents
}
}.frame(height: 100)
Starting from iOS 14 beta1 & XCode 12 beta1 you will be able to wrap LazyHStack in a ScrollView to create a horizontal lazy list of items, i.e., each item will only be loaded on demand:
ScrollView(.horizontal) {
LazyHStack {
ForEach(0...50, id: \.self) { index in
Text(String(index))
.onAppear {
print(index)
}
}
}
}
To make a horizontal scrollable content, you can wrap a HStack inside a ScrollView:
ScrollView {
HStack {
ForEach(0..<10) { i in
Text("Item \(i)")
Divider()
}
}
}
.frame(height: 40)
You can use .horizontal property and use custom elements. For me I use cusomt CircleView
var body: some View {
VStack{
Divider()
ScrollView(.horizontal){
HStack(spacing:10){
ForEach(0..<10){
index in
CircleView(label: "\(index)")
}
}.padding()
}.frame(height:100)
Divider()
Spacer()
}
}
}
//struct CircleView:View
#State var label:String
var body:some View {
ZStack{
Circle()
.fill(Color.yellow)
.frame(width: 70, height: 70)
Text(label)
}
}
}