How can I take a frame and divide its width by 2? - arrays

I am trying to set up a view that displays out a mathematical equation, with exponents being adjusted for size. I ran into an issue where after applying a scaleEffect to the item in the exponent, the frame of that view remained the same size, leaving empty space where there shouldn't be any.
My goal is very simple: I just need to take the proposed frame width and divide it by 2 in order to match the scaleEffect() applied to its contents. Anything like this works:
.frame(width: originalWidth / 2)
I quickly discovered that there is unexplainably not a single way to just read out the proposed, standard, ideal frame that would be applied to a view in absence of .frame().
That is, aside from Geometry Reader, which is a nightmare to deal with from how it resizes everything, how the data is only accessible within itself, and the fact code inside runs after a .frame() modifier is already applied to a view.
My starting point:
struct StaticEquationView: View {
//Initialization of the master controller
#StateObject var data = DataController()
#State var frameWidth: CGFloat = .zero
#State var frameApplied: Bool = false
var body: some View {
HStack(spacing: 0) {
ForEach($data.blockArray) { $block in
DisplayBlockView(data: data, block: $block)
}
}
}
}
struct DisplayBlockView: View {
#ObservedObject var data: DataController
#Binding var block: Block
var body: some View {
Text(block.label)
HStack(spacing: 0) {
ArrayView(data: data, array: $block.exp)
}
//HERE
.scaleEffect(0.5)
.offset(CGSize(width: 0, height: -10))
}
}
struct ArrayView: View {
#ObservedObject var data: DataController
#Binding var array: [Block]
var body: some View {
HStack {
ForEach($array) { $block in
DisplayBlockView(data: data, block: $block)
}
}
}
}
And here is my unsuccessful attempt at shrinking the frame. Somehow this code would uncontrollably expand the height of the view and shrink the subviews of each individual DisplayBlockView inside ArrayView, all while still keeping some spacing between the edges and the contents of ArrayView.
struct DisplayBlockView: View {
#ObservedObject var data: DataController
#Binding var block: Block
#State var frameWidth: CGFloat = .zero
#State var frameApplied: Bool = false
var body: some View {
Text(block.label)
HStack(spacing: 0) {
ArrayView(data: data, array: $block.exp)
.overlay(GeometryReader { geo in
Text("")
.onAppear() {
frameWidth = geo.size.width * 0.5
frameApplied = true
}
})
}
.frame(width: frameApplied ? frameWidth : nil, height: nil)
.scaleEffect(0.5)
.offset(CGSize(width: 0, height: -10))
.offset(block.cOffset)
.border(Color.gray)
}
}

Related

How to show Firebase images in SwiftUI using an ImageSlider or something similar

I have been trying to do this for some time, but have not been able to achieve my goal. I would like to show images stored at Firebase in an Image Slider or carousel in SwiftUI. I am able to fetch and show a single image (that's a different thing). However, unable to load images from firebase to slider. The first part is to get url of images which I am not getting how to do it.
Update
This loads images but cant auto scroll or auto iterate, images load one after another than stops:
func downImage(){
let imageRef = Storage.storage()
let loc = imageRef.reference().child("Images")
loc.listAll{(result,error) in
if error != nil{
print("Ooops : ", error)
return
}
for item in result!.items{
let count = result?.items.count ?? 5
//provides actual count ie 3
print("number of Images: ", self.count)
print("images url : " , item)
item.getData(maxSize: 15 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
self.count = count
// self.actualImage.append(self.image)
self.image = UIImage(data: data as Data)!
}}}}}}
In Slider, images are shown but stops after loading third image:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
/* private let timer = Timer.publish(every: 3, on: .main, in: .common)
.autoconnect() */
var body: some View {
TabView(){
/* ForEach(imageList.actualImage, id: \.self) { item in
if foreach loop is used with actualImage appending Images no
image is shown or Index out of range error */
Image(uiImage: imageList.image)
.resizable()
.scaledToFill()
}
.tabViewStyle(PageTabViewStyle())
.animation(.easeInOut(duration: 3))
.onAppear(){
imageList.downImage()
}
}
If I apply the timer (commented out) with other settings it keeps updating selected image 0, selected image 1 but image never changes. Need images to auto scroll in this slider.
As I understand your code, you are downloading the string url from Firebase into
#Published var imageURL: [String] = []. This does not download the image data, just the url string.
In your loadImageURL, try this, to store all the url strings into your imageURL array:
DispatchQueue.main.async {
if let linq = link {
self.imageURL.append(linq)
print("url link : ", self.imageURL)
}
In your ImageSlider, use something like this, instead of your Image(item):
AsyncImage(url: URL(string: item))
also, change #ObservedObject var imageList = FireImageModel() to #StateObject var imageList = FireImageModel()
and, do not use !, anywhere in your code, it is a recipe for disaster.
EDIT-1:
Given your new code, try this simple example code, to try to find where your problem is:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
var body: some View {
ScrollView {
ForEach(imageList.actualImage, id: \.self) { item in
Image(uiImage: item)
.resizable()
.frame(width: 222, height: 222)
}
.onAppear {
imageList.downImage()
}
}
}
}
Also, do not use ! anywhere in your code.
If that works, try this:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
var body: some View {
VStack {
if imageList.actualImage.count > 0 {
TabView {
ForEach(imageList.actualImage, id: \.self) { item in
Image(uiImage: item)
.resizable()
.scaledToFill()
}
}
.tabViewStyle(PageTabViewStyle())
} else {
ProgressView()
}
}
.onAppear {
imageList.downImage()
}
}
}
You may also want to clean your FireImageModel, such as:
class FireImageModel: ObservableObject {
#Published var actualImage: [UIImage] = []
func downImage() {
let imageRef = Storage.storage()
let loc = imageRef.reference().child("Images")
loc.listAll{(result,error) in
if error != nil{
print("Ooops : ", error)
return
}
if let images = result {
print("number of Images: ", images.items.count)
for item in images.items {
print("images url : " , item)
item.getData(maxSize: 15 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
if let theData = data, let image = UIImage(data: theData) {
self.actualImage.append(image)
}
}
}
}
}
}
}
}
I was able to display images in slider final prob was auto iteration. ForEach was throwing index out of range error(known prob) when directly using an array having image data so ans by #workingdogUkraine helped to resolve the error by putting check before entering the foreach. Auto iteration is done. Look into updated clean FireImageModel model by WorkingDog, below is the slider struct:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
#State private var currentIndex = 0
#State var zoomed = false
private let timer = Timer.publish(every: 3, on: .main, in: .common)
.autoconnect()
var body: some View {
// 2
VStack {
if imageList.actualImage.count > 0 {
GeometryReader{ proxy in
TabView(selection: $currentIndex){
ForEach(imageList.actualImage.indices, id: \.self) {
item in
let sot = imageList.actualImage[item]
Image(uiImage: sot)
.resizable()
.scaledToFill()
.onTapGesture {
self.zoomed.toggle()
}
.scaleEffect(self.zoomed ? 1.73 : 1)
}
.onReceive(timer, perform: {_ in
withAnimation(.spring(response: 1,
dampingFraction: 1, blendDuration: 1)){
currentIndex = currentIndex <
imageList.actualImage.count ? currentIndex + 1 : 0
}
})
}
.tabViewStyle(PageTabViewStyle())
.frame(width: proxy.size.width, height: proxy.size.height/3)
}
}else {
ProgressView()
}
}.background(.ultraThinMaterial)
.onAppear {
imageList.downImage()
}
}}
Images appear with non noticeable animation :), will like to add some cool animation, after every 3 seconds image changes

SwiftUI: String property of an object is not displaying in Text, and how would you edit a string in an object?

I am very new to programming in Swift. So I'm trying to come up with a time management program. I have posted some code that have been derived from my project that is a work in progress, and I'm trying to troubleshoot some issues that I'm having that come from my lack of knowledge regarding Swift and SwiftUI. I would like to ask two questions here, but if you only have the answer to just one of them, I would greatly appreciate it.
So in my ContentView, I'm trying to display the taskName of the object with ID 0 using a Text in a VStack -- however, it is not displaying, and I'm not sure of the reason why. I can display the taskLength by putting it inside the String method, but taskName is not coming up when I attempt to display it.
Also I'm attempting to change the taskName of Task(id: 0) that is being passed into display2 directly from the display2, but I'm not sure if the taskName of Task(id: 0) is actually being changed, or it's only the taskName of #State var task:Task in display2 that is being changed -- based on my intuitions, I would think the latter case is actually happening. In that case, is there a way to directly edit the taskName of Task(id: 0) from display2?
import SwiftUI
import Foundation
import Combine
struct Task: Hashable, Codable, Identifiable {
var id: Int
var taskName: String = ""
var taskLength: Int = 0
var isBreak : Bool = false
}
class ModelData : ObservableObject{
#Published var tasks: [Task] = [
Task(id: 0,taskName: "Test", taskLength: 34, isBreak: false),
Task(id: 1,taskName: "Math", taskLength: 30, isBreak: false),
Task(id: 2,taskName: "Science", taskLength: 40, isBreak: false)
]
}
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(Task(id: 0).taskName)
display2(task:Task(id: 0))
}
}
}
struct display2: View{
#State var task:Task
var body: some View {
TextField("New task",text: $task.taskName)
}
}
The problem is here:
Text(Task(id: 0).taskName)
Here, you're creating a new Task, with an id of 0. This is not the first task inside your ModelData's tasks array.
Instead, reference the first task via subscript []:
Text(modelData.tasks[ /* index of task */ ].taskName)
Normally you can just put 0 here to get the first Task. However, you said you actually want the Task with an id of 0. You can do this via firstIndex(where:).
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(
modelData.tasks[getTaskIndexFrom(id: 0)] /// access
.taskName
)
Display2( /// see https://stackoverflow.com/a/67064699/14351818
task: $modelData.tasks[getTaskIndexFrom(id: 0)]
)
}
}
func getTaskIndexFrom(id: Int) -> Int {
/// get first index of a task with the specified `id`
if let firstIndex = modelData.tasks.firstIndex(where: { $0.id == 0 }) {
return firstIndex
} else {
return 0
}
}
}
struct Display2: View{
#Binding var task: Task /// need a Binding here
var body: some View {
TextField("New task", text: $task.taskName)
}
}
Ok, your second question:
In that case, is there a way to directly edit the taskName of Task(id: 0) from display2?
Yep! Just use #Binding on Display2's task. This way, all changes will be synced back to your modelData.
In ContentView you used just Task(), but you have to use modelData for #Published var tasks in ModelData.
Task(id: 0).taskName -> modelData.tasks[1].taskName
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(modelData.tasks[1].taskName)
.foregroundColor(.blue)
display2(task:Task(id: 0))
}
}
}
Also, as long as you use #EnvironmentObject, you need to add .environmentObject to the main as well.
(The code below is an example of the SwiftUI life cycle)
import SwiftUI
#main
struct ReplyToStackoverflowApp: App {
var modelData: ModelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}

How to update variables in ContentView with SheetView function call?

I have a code with a sheet view and a normal view. When I press a button within my sheet view I make an API call. This API call then updates some variables which I'm trying to display in my regular view using ´ForEach´. However, when I make the call in the sheet view and close it down, the array does not seem to update in my normal view. My view just remains blank (except for displaying the button that says "Show sheet". How do I make the array update so that it isn't blank?
Here is my regular view:
// MARK: - Schedule View
struct ScheduleView: View {
#State var selectedTab = 0
#State private var showingSheet = true
var body: some View {
GeometryReader { geo in
VStack {
ForEach(SheetView().vm.Trips, id: \.self) { dict in
Text(dict["Origin"]!) // I want this varible to update, but I doesn't
Text(dict["Destination"]!) // It instead remains blank
}
Button("Show sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
.frame(width: geo.size.width*0.7, height: geo.size.height*0.06)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(11)
.position(x: geo.size.width/2, y: geo.size.height/2)
// MARK: - Padding funkar inte
}
}.padding()
}
}
And here is my sheet view:
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var vm: PlanTripViewModel = PlanTripViewModel()
#State var selected = 0
var body: some View {
GeometryReader {geo in
ZStack{
VStack {
TextField("From", text: $vm.origin.input).padding()
TextField("To", text: $vm.dest.input).padding()
TextField("00:00", text: $vm.arrivalTime).padding()
TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
Button("Add trip") {
vm.fetchStatus = .start // This starts the API call in another file of mine
presentationMode.wrappedValue.dismiss() // This closes the sheet view
}.padding()
}.foregroundColor(.blue)
}
}
}
}
Right now, you're making a new SheetView instance on every single ForEach call -- it's not the same one that you're using in your sheet call.
To solve this, you'll want to store the state in your parent view and give the sheet view a reference to it.
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: PlanTripViewModel //<-- Here
#State var selected = 0
var body: some View {
GeometryReader {geo in
ZStack{
VStack {
TextField("From", text: $vm.origin.input).padding()
TextField("To", text: $vm.dest.input).padding()
TextField("00:00", text: $vm.arrivalTime).padding()
TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
Button("Add trip") {
vm.fetchStatus = .start // This starts the API call in another file of mine
presentationMode.wrappedValue.dismiss() // This closes the sheet view
}.padding()
}.foregroundColor(.blue)
}
}
}
}
struct ScheduleView: View {
#State var selectedTab = 0
#State private var showingSheet = true
#StateObject var vm: PlanTripViewModel //<-- Here
var body: some View {
GeometryReader { geo in
VStack {
ForEach(vm.Trips, id: \.self) { dict in
Text(dict["Origin"]!)
Text(dict["Destination"]!)
}
Button("Show sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView(vm: vm) //<-- Here
}
.frame(width: geo.size.width*0.7, height: geo.size.height*0.06)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(11)
.position(x: geo.size.width/2, y: geo.size.height/2)
// MARK: - Padding funkar inte
}
}.padding()
}
}
(Note: you may know this already, but force unwrapping your dictionary values with ! will cause a crash if the keys don't exist. You may want to use optional binding (if let) or another safe check to make sure they exist.)

SwiftUI: List, ForEach, indices and .onDelete not working when using TextField() - (Index out of range) [duplicate]

Environment
Xcode 11.2.1 (11B500)
Problem
In order to implement editable teble with TextField on SwiftUI, I used ForEach(0..<items.count) to handle index.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i])
}
}
}
}
However, problems arise if the table is changed to be deleteable.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i]) // Thread 1: Fatal error: Index out of range
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐶" from animals
}
}
}
}
Thread 1: Fatal error: Index out of range when delete item
🐶 has been removed from animals and the ForEach loop seems to be running twice, even though animals.count is 1.
(lldb) po animals.count
1
(lldb) po animals
▿ 1 element
- 0 : "🐱"
Please give me advice on the combination of Foreach and TextField.
Thanks.
Ok, the reason is in documentation for used ForEach constructor (as you see range is constant, so ForEach grabs initial range and holds it):
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
So the solution would be to provide dynamic container. Below you can find a demo of possible approach.
Full module code
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
VStack {
HStack {
EditButton()
Button(action: { self.animals.append("Animal \(self.animals.count + 1)") }, label: {Text("Add")})
}
List {
ForEach(animals, id: \.self) { item in
EditorView(container: self.$animals, index: self.animals.firstIndex(of: item)!, text: item)
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐶" from animals
}
}
}
}
}
struct EditorView : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
it is because editbutton is IN your list. place it ouside or better in navigationbar.

SwiftUI watchOS How to display array content as subviews in a single View?

I'm doing an Apple Watch app, using SwiftUI. The idea is to display different activity content in a single watch ContentView using four individual subviews within the ContentView Body View. I don't want to use a List to display the activity content, but rather multiple custom views within the ContentView.
I would like each individual subview to display unique content from my model.
My model is called QuadActivity and has the following content:
struct QuadActivity: Identifiable {
let id = UUID()
var activityImage: Image
var activityTitle: String
}
I currently have created an extension to QuadActivity as follows, to hold some hardcoded test data:
extension QuadActivity {
static func all() -> [QuadActivity] {
return [
QuadActivity(activityImage: activityImage1, activityTitle: "activity1"),
QuadActivity(activityImage: activityImage2, activityTitle: "activity2"),
QuadActivity(activityImage: activityImage3, activityTitle: "activity3"),
QuadActivity(activityImage: activityImage4, activityTitle: "activity4")]
}
}
My ContentView.swift Body view is made up of a VStack with 2 HStacks embedded. Each HStack contains 2 of my subviews with miscellaneous spacers and padding modifiers. Each of the subviews should display the content of one of the array elements from an instance property:
var activityArrayEntry = QuadActivity.all()
Thus HStack 1 should display activityImage1 and activity1 and activityImage2 and activity2. The other HStack should display the array elements for items 3 and 4.
I can't figure out how to access each of the activityArrayEntry array elements and display each one in one of the subviews.
I was thinking I could use a:
ForEach(activityArrayEntry) { activity in
VStack and embedded HStack code here}
and display the subview content by looping through the ForEach above.
However, since all my VStack and HStack and subview code is within the ForEach loop, the same array element activity content would be displayed for all subviews because the loop encompasses all the view information for a single pass of the loop. I want each subview to display one of the unique array element's content.
If I move the ForEach within the ZStack and HStack code for each HStack subview display section, it will loop through the array entries, but the loop won't encompass all the subviews code and I won't get all the subviews to display only the activity array content from 1 array element.
Maybe using a ForEach loop is not the way. Is there another way to access the individual array elements from my instance variable such that each unique array element is used only in 1 of the subviews?
Again, how do I get the overall ContentView.swift to display the four subviews within the ZStack and HStack structure so that each subview displays the activity content of only 1 of the array elements.
Here is my ContentView so far. Note a number of commented lines that I will eventually comeback to in order to use an Observed Object model approach from a #Published Observable Object of my model. This will eventually be (maybe) the approach instead of the function all() from my model that I'm using now with hardcoded data to test the data flow in my app... thus the original question/issue.
Note
Call to QuadView() is just a call to an extracted subview where I define the display of the subview (simple Image and Text):
import SwiftUI
struct ContentView: View {
// #ObservedObject var quadViewVM = QuadViewVM()
var activityArrayEntry = QuadActivity.all()
var body: some View {
ZStack {
HStack {
Divider()
.frame(height: 175.0)
}
.edgesIgnoringSafeArea(.horizontal)
ForEach(activityArrayEntry) { activity in
VStack {
HStack(alignment: .center) {
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail(content: , newActivity: false)) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
Spacer()
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail()) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal, 15.0)
.padding(.bottom, -10.0)
Divider()
HStack(alignment: .center) {
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail()) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
Spacer()
QuadView(activityTitle: "\(activity.activityTitle)", activityImage: activity.activityImage)
// NavigationLink(destination: QuadDetail()) {
//
// }
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
}
.padding([.leading, .bottom, .trailing], 15.0)
.padding(.top, -10.0)
.padding(.top, 30.0)
.edgesIgnoringSafeArea(.horizontal)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Instead of static QuadActivity.all() you can store your data in the ViewModel:
class QuadViewVM: ObservableObject {
#Published var quadActivities: [QuadActivity] = [
QuadActivity(activityImage: activityImage1, activityTitle: "activity1"),
QuadActivity(activityImage: activityImage2, activityTitle: "activity2"),
QuadActivity(activityImage: activityImage3, activityTitle: "activity3"),
QuadActivity(activityImage: activityImage4, activityTitle: "activity4"),
]
}
In your ContentView you can create a grid by using two ForEach loops (as it's a 2D grid):
struct ContentView: View {
#ObservedObject var quadViewVM = QuadViewVM()
let columnCount = 2
var rowCount: Int {
quadViewVM.quadActivities.count / columnCount
}
var body: some View {
ZStack {
// Horizontal divider
VStack {
Divider()
}
.edgesIgnoringSafeArea(.horizontal)
// Vertical divider
HStack {
Divider()
}
.edgesIgnoringSafeArea(.vertical)
activityGrid
}
}
var activityGrid: some View {
VStack {
ForEach(0 ..< self.rowCount) { row in
HStack {
ForEach(0 ..< self.columnCount) { column in
self.quadView(row: row, column: column)
}
}
}
}
}
func quadView(row: Int, column: Int) -> some View {
let activity = quadViewVM.quadActivities[row * columnCount + column]
return QuadView(activity: activity)
.frame(width: 85.0, height: 100.0)
.buttonStyle(PlainButtonStyle())
}
}
The QuadView is extracted to another function to make it more readable and easier to apply view modifiers.
I'd also recommend passing the whole QuadActivity variable to the QuadView (instead of its single components) - specifically when you need them all:
struct QuadView: View {
let activity: QuadActivity
var body: some View {
...
}
}

Resources