SwiftUI Crash when calling scrollTo method when view is disappearing - swiftui

I try to make my ScrollView fixed in a specific place, but the scrollTo method will cause the application to crash.
How to make the ScrollView stay in a fixed place?
I want to control the switching of views by MagnificationGesture to switch from one view to another.
But when Scroll() disappdars, the app crashs.
struct ContentView: View {
#State var tabCount:Int = 1
#State var current:CGFloat = 1
#State var final:CGFloat = 1
var body: some View {
let magni = MagnificationGesture()
.onChanged(){ value in
current = value
}
.onEnded { value in
if current > 2 {
self.tabCount += 1
}
final = current + final
current = 0
}
VStack {
VStack {
Button("ChangeView"){
self.tabCount += 1
}
if tabCount%2 == 0 {
Text("some text")
}else {
Scroll(current: $current)
}
}
Spacer()
HStack {
Color.blue
}
.frame(width: 600, height: 100, alignment: .bottomLeading)
}
.frame(width: 600, height: 400)
.gesture(magni)
}
}
This is ScrollView, I want it can appear and disappear. When MagnificationGesture is changing, scrollview can keep somewhere.
struct Scroll:View {
#Binding var current:CGFloat
let intRandom = Int.random(in: 1..<18)
var body: some View {
ScrollViewReader { proxy in
HStack {
Button("Foreword"){
proxy.scrollTo(9, anchor: .center)
}
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<20) { item in
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 40)
.overlay(Text("\(item)").foregroundColor(.white))
.id(item)
}
}
.onChange(of: current, perform: { value in
proxy.scrollTo(13, anchor: .center)
})
}
}
}
}

Related

How to get rid of NavigationLink space

I have a problem with space occupied by NavigationLink. Following code:
struct EditView: View {
var body: some View {
NavigationView {
Form {
Section("Colors") {
ColorList(colors: viewModel.game.gameColors)
}
}
}
}
}
struct ColorList: View {
let colors: [String]
private let gridItemLayout = [GridItem(.adaptive(minimum: 44))]
var body: some View {
ScrollView {
LazyVGrid(columns: gridItemLayout) {
ForEach(colors, id: \.self) { colorName in
Meeple(colorName: colorName)
}
.padding(.vertical, 2)
}
}
}
}
// Meeple is just an image
struct Meeple: View {
// ...
var body: some View {
Image("meeple.2.fill")
.resizable()
.padding(5)
.foregroundColor(color.color)
.background(color.backgroundColor)
.frame(width: 44, height: 44, alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 5))
.shadow(color: .primary, radius: 5)
}
}
Produces a good result:
As soon as I add a NavigationLink around the ColorList like so
Section("Colors") {
NavigationLink(destination:
MultiColorPickerView(
selection: $viewModel.game.colors.withDefaultValue([])
)
) {
ColorList(colors: viewModel.game.gameColors)
}
}
The result looks weird:
There's plenty of space left. Why does it break after 3 items? And how can I make it to show more in one line?
add .frame(maxWidth: .infinity) to your ColorList.

How to draw a rectangle to a specific area of the screen based on user search input?

I am trying to make a program where the user enters a certain set of characters into the search bar (for example "AP1") and the program draws a rectangle on top of an image I have.
I will have a bunch of if statements testing what the user entered and giving the coordinates for where the rectangle will be drawn. I am just having trouble with the "scopes" and the ZStack and VStack for the image overlay not wanting to cooperate with how I have the if statement(s) set up. Here is my entire program:
This is my third day doing any type of iOS development
import SwiftUI
struct ContentView: View {
private var listOfBins = binList
#State var searchText = ""
var body: some View {
// MAP
VStack {
Image("map")
.resizable()
.scaledToFit()
.position(x: 195, y: 175)
.overlay(ImageOverlay(), alignment: .bottomTrailing)
Spacer()
}
NavigationView {
List {
ForEach(bins, id: \.self) { bin in
HStack {
Text(bin.capitalized)
.textCase(.uppercase)
Spacer()
Image(systemName: "figure.walk")
.foregroundColor(Color.blue)
}
.padding()
}
}
.searchable(text: $searchText)
.navigationTitle("Bins")
if (searchText.elementsEqual("AP1")) {
drawBox(width: 50, height: 50, x: 50, y: 50)
}
}
}
func drawBox(width: Int, height: Int, x: Int, y: Int) -> Rectangle{
struct ImageOverlay: View{
var body: some View {
ZStack {
Rectangle()
.fill(.green)
.frame(width: 100, height: 100)
.position(x: 200, y: 300)
}
}
}
}
// DISPLAY LIST OF BINS AND SEARCH BAR
var bins: [String] {
let upBins = listOfBins.map {$0.uppercased()}
return searchText == "" ? upBins : upBins.filter{
$0.contains(searchText.uppercased())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
Could not run your provided code, so I replicated a temp view.
Is this something you wanted? (code is below the image)
struct SSContentView: View {
#State var searchText = ""
var images = ["Swift", "Ww", "Luffy"]
var body: some View {
NavigationView {
List {
TextField("Search Here", text: $searchText)
ForEach(0...5, id: \.self) { _ in
ForEach(images, id: \.self) { image in
ZStack {
Image(image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
.overlay {
if searchText == image {
OverlayImage
}
}
}
}
}
}
.navigationTitle("My Pictures")
}
}
var OverlayImage: some View {
ZStack {
Rectangle()
.fill(.clear)
.frame(width: 60, height: 60)
.border(.green)
}
}
}

How do I create this effect using SwiftUI

See this gif
What is happening on this gif is: the finger touches the white area on any point on the right part and drags to the left. As the finger drags, these 3 buttons zoom in and appear.
Assuming the buttons zoom from scale = 0 to scale = 1, Does not matter if I release the finger when the scale is at any value bigger than 0. The buttons will zoom to scale 1 automatically.
NOTE: The animation is slow because I am sliding the finger slowly, but the animation follows the finger drag. If I drag left, buttons zoom in, if I drag right, buttons zoom out.
How do I do that with SwiftUI.
I have this code so far for the whole thing.
struct FileManagerPanelListItem: View {
var body: some View {
ZStack{
VStack {
ZStack {
Image("image")
.resizable()
.frame(width: 190, height: 190, alignment: .center)
HStack(alignment:.center){
FileManagerPanelButton("share", Color.white, Color.gray, {})
FileManagerPanelButton("duplicate", Color.white, Color.gray, {})
FileManagerPanelButton("delete", Color.white, Color.red, {})
}
.frame(maxWidth:.infinity)
}
Text("name")
.frame(width:170)
.background(Color.yellow)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
Text("notes")
.frame(width:170)
.background(Color.blue)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth:240, maxHeight: 240)
}
}
struct FileManagerPanelListItem_Previews: PreviewProvider {
static var previews: some View {
FileManagerPanelListItem()
.previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Mini"))
}
}
struct FileManagerPanelButton: View {
typealias runOnSelectHandler = ()->Void
private var runOnSelect:runOnSelectHandler?
private var label:String
private var fgColor:Color
private var bgColor:Color
init(_ label: String,
_ fgColor:Color,
_ bgColor:Color,
_ runOnSelect: runOnSelectHandler? ) {
self.label = label
self.fgColor = fgColor
self.bgColor = bgColor
self.runOnSelect = runOnSelect
}
var body: some View {
Button(action: {
runOnSelect?()
}, label: {
Text(label)
.avenir(.ROMAN, size: 16)
.foregroundColor(fgColor)
.frame(height:60)
.frame(maxWidth:.infinity)
})
.frame(maxWidth:.infinity)
.background(bgColor)
.cornerRadius(10)
}
}
any ideas?
Something like this should work:
// view cannot be scaled to zero, so we take some small value near
private let minScale: CGFloat = 0.001
// relative distance from right side of view to open and from left side to close
private let effectiveDragSidePart: CGFloat = 0.1
struct FileManagerPanelListItem: View {
#State
var scale: CGFloat = minScale
#State
var opened = false
var body: some View {
ZStack{
VStack {
ZStack {
Image("profile")
.resizable()
.frame(width: viewWidth, height: 190, alignment: .center)
.gesture(
DragGesture()
.onChanged { value in
guard shouldProcessStartLocation(value.startLocation) else { return }
scale = translationToScale(value.translation)
}
.onEnded { value in
guard shouldProcessStartLocation(value.startLocation) else { return }
withAnimation(.spring()) {
if shouldRestore(value.predictedEndTranslation) {
scale = opened ? 1 : minScale
} else {
scale = opened ? minScale : 1
opened.toggle()
}
}
}
)
HStack(alignment:.center){
FileManagerPanelButton("share", Color.white, Color.gray, {})
.scaleEffect(scale, anchor: .center)
FileManagerPanelButton("duplicate", Color.white, Color.gray, {})
.scaleEffect(scale, anchor: .center)
FileManagerPanelButton("delete", Color.white, Color.red, {})
.scaleEffect(scale, anchor: .center)
}
.frame(maxWidth:.infinity)
}
}
}
.frame(maxWidth:240, maxHeight: 240)
}
private let viewWidth: CGFloat = 190
private func shouldRestore(_ predictedEndTranslation: CGSize) -> Bool {
if opened {
return translationToScale(predictedEndTranslation) == 1
} else {
return translationToScale(predictedEndTranslation) == minScale
}
}
private func shouldProcessStartLocation(_ startLocation: CGPoint) -> Bool {
if opened {
return startLocation.x < viewWidth * effectiveDragSidePart
} else {
return startLocation.x > viewWidth * (1 - effectiveDragSidePart)
}
}
private func translationToScale(_ translation: CGSize) -> CGFloat {
if opened {
return max(0.001, min(1, 1 - translation.width / viewWidth))
} else {
return max(0.001, -translation.width / viewWidth)
}
}
}
You can setup .spring() if the default one is not springy enough

Crash due index out of range -- although I'm sure index is not out of range

I have a parent view whose child view is any given index of an array. The index of the array is scrolled through by tapping buttons that increment or decrement the index which is stored in a State property.
However when the view is first initialized I get a crash, even though the State's initial value is always 0.
What is going on?
Code can be copied and pasted to reproduce error
import SwiftUI
struct ContentView: View {
#State private var shouldShowQuotes = false
var body: some View {
ZStack {
Color.orange
VStack {
Button(action: showQuotes){
Text("Get Quotes").bold()
.frame(maxWidth: 300)
}
// .controlProminence(.increased) //Safe to uncomment if Xcode 13
// .buttonStyle(.bordered)
// .controlSize(.large)
}
.fullScreenCover(isPresented: $shouldShowQuotes) {
QuoteScreen()
}
}.ignoresSafeArea()
}
private func showQuotes() {
self.shouldShowQuotes.toggle()
}
}
struct QuoteScreen: View {
#State private var quoteIndex = 0
var currentQuote: Quote {
return dummyData[quoteIndex]
}
var body: some View {
ZStack {
Color.orange
VStack {
QuoteView(quote: currentQuote)
Spacer()
HStack {
Button(action: degress) {
Image(systemName: "arrow.left.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
Button(action: progress) {
Image(systemName: "arrow.right.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
}
.padding(28)
//.buttonStyle(.plain) Safe to uncomment if Xcode 13
}
}.ignoresSafeArea()
}
private func progress() {
quoteIndex += 1
}
private func degress() {
quoteIndex -= 1
}
}
struct QuoteView: View {
#State private var showQuotes = false
let quote: Quote
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.stroke(lineWidth: 2)
VStack {
Text(quote.quote)
frame(maxWidth: 300)
Text(quote.author)
.frame(maxWidth: 300, alignment: .trailing)
.foregroundColor(.secondary)
}
}.navigationBarHidden(true)
.frame(height: 400)
.padding()
}
}
let dummyData = [Quote(quote: "The apple does not fall far from the tree", author: "Lincoln", index: 1),
Quote(quote: "Not everything that can be faced can be changed, but be sure that nothing can change until it is faced", author: "Unknown", index: 2),
Quote(quote: "Actions are but intentions", author: "Muhammad", index: 3)
]
struct Quote: Codable {
let quote: String
let author: String
let index: Int
}
When using arrays you always have to check that the element at the chosen index exist. This is how
I tested and modify your code to make it work.
(note: although this is just a test with dummyData, you need to decide if you want to scroll through the array index, or the Quote-index value, and adjust accordingly)
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
let dummyData = [
Quote(quote: "the index zero quote", author: "silly-billy", index: 0),
Quote(quote: "The apple does not fall far from the tree", author: "Lincoln", index: 1),
Quote(quote: "Not everything that can be faced can be changed, but be sure that nothing can change until it is faced", author: "Unknown", index: 2),
Quote(quote: "Actions are but intentions", author: "Muhammad", index: 3)
]
struct ContentView: View {
#State private var shouldShowQuotes = false
var body: some View {
ZStack {
Color.orange
VStack {
Button(action: showQuotes){
Text("Get Quotes").bold()
.frame(maxWidth: 300)
}
// .controlProminence(.increased) //Safe to uncomment if Xcode 13
// .buttonStyle(.bordered)
// .controlSize(.large)
}
.fullScreenCover(isPresented: $shouldShowQuotes) {
QuoteScreen()
}
}.ignoresSafeArea()
}
private func showQuotes() {
self.shouldShowQuotes.toggle()
}
}
struct QuoteScreen: View {
#State private var quoteIndex = 0
#State var currentQuote: Quote = dummyData[0] // <--- here, do not use "quoteIndex"
var body: some View {
ZStack {
Color.orange
VStack {
QuoteView(quote: $currentQuote) // <--- here
Spacer()
HStack {
Button(action: degress) {
Image(systemName: "arrow.left.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
Button(action: progress) {
Image(systemName: "arrow.right.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
}
.padding(28)
//.buttonStyle(.plain) Safe to uncomment if Xcode 13
}
}.ignoresSafeArea()
}
// you will have to adjust this to your needs
private func progress() {
let prevValue = quoteIndex
quoteIndex += 1
if let thisQuote = dummyData.first(where: { $0.index == quoteIndex}) { // <--- here
currentQuote = thisQuote
} else {
quoteIndex = prevValue
}
}
// you will have to adjust this to your needs
private func degress() {
let prevValue = quoteIndex
quoteIndex -= 1
if let thisQuote = dummyData.first(where: { $0.index == quoteIndex}) { // <--- here
currentQuote = thisQuote
} else {
quoteIndex = prevValue
}
}
}
struct QuoteView: View {
#State private var showQuotes = false
#Binding var quote: Quote // <--- here
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.stroke(lineWidth: 2)
VStack {
Text(quote.quote)
.frame(maxWidth: 300) // <--- here missing leading "."
Text(quote.author)
.frame(maxWidth: 300, alignment: .trailing)
.foregroundColor(.secondary)
}
}.navigationBarHidden(true)
.frame(height: 400)
.padding()
}
}
struct Quote: Identifiable {
let id = UUID()
var quote: String
var author: String
var index: Int
}
This crash is not caused by the array access but by a typo in your code. You can see that if you run it in the simulator and look at the stack trace. It gets in an endless loop in the internals of SwiftUI. The reason is the missing dot before the frame modifier:
struct QuoteView: View {
#State private var showQuotes = false
let quote: Quote
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.stroke(lineWidth: 2)
VStack {
Text(quote.quote)
frame(maxWidth: 300) << !!!!! missing dot
Text(quote.author)
.frame(maxWidth: 300, alignment: .trailing)
.foregroundColor(.secondary)
}
}.navigationBarHidden(true)
.frame(height: 400)
.padding()
}
}
This calls the frame method on the QuoteView and not on the Text - which is an invalid operation.

How to make SwiftUI to forget internal state

I have a view like following
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
// Common Elements
content
// More Common Elements
}
}
}
}
When I call this from another view like
A(nextInnerView())
two things happen. Firstly, as the size of the content element changes ScrollView animates the transition. Secondly, if you scroll down and then change the content the scrolling position does not reset.
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
The origin of this behaviour is in SwiftUI rendering optimisation, that tries to re-render only changed part, so approach is to identify view A (to mark it as completely changed) based on condition that originated in interview changes, alternatively it can be identified just by UUID().
struct TestInnerViewReplacement: View {
#State private var counter = 0
var body: some View {
VStack {
Button("Next") { self.counter += 1 }
Divider()
A(content: nextInnerView())
.id(counter) // << here !!
}
}
private func nextInnerView() -> AnyView {
AnyView(Group {
if counter % 2 == 0 {
Text("Text Demo")
} else {
Image(systemName: "star")
.resizable()
.frame(width: 100, height: 100)
}
})
}
}
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(0..<5) { _ in // upper content demo
Rectangle().fill(Color.yellow)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
content
ForEach(0..<10) { _ in // lower content demo
Rectangle().fill(Color.blue)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
}
}
}
}