Swiftui ObservableObject with array not update view - swiftui

I have a problem with Array using ObservableObject in my view. I have an empty array. I call a function at page onAppear. When the data is returned, the view does not update with the new data in array:
class NewsState: ObservableObject {
private let base: String = "api"
let objectWillChange = ObservableObjectPublisher()
#Published var wagsList: Array<UserSlider> = [] {
willSet {
objectWillChange.send()
}
}
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
for i in resp {
let userSlider = UserSlider(id: i.id, uid: i.uid, image: i.image)
self.wagsList.append(userSlider)
}
}
}
}
In my view I have this:
HStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
if(self.newsState.wagsList.count != 0) {
ForEach(self.newsState.wagsList, id: \.self) { wags in
VStack {
HStack {
URLImage(URL(string: "\(wags.image)")!, expireAfter: Date(timeIntervalSinceNow: 10)) { proxy in
proxy.image
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(Circle())
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 2)
)
.contentShape(Circle())
}.frame(width: 62, height: 62)
}
HStack {
Text("10K")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Bold", size: 15))
}
HStack {
Text("followers")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Normal", size: 15))
}
}
}
} else {
//loader
}
}.onAppear(perform: initPage)
}
}
What am I doing wrong? I see that the problem is caused by ScrollView.

Try this one
class NewsState: ObservableObject {
private let base: String = "api"
#Published var wagsList: Array<UserSlider> = []
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
let results = resp.map { UserSlider(id: $0.id, uid: $0.uid, image: $0.image) }
DispatchQueue.main.async {
self.wagsList = results
}
}
}
}

As it is not clear to me where the error might lay. It could be either in getList or in your View.
This is an easy example of how it works with a Published and ObserverdObject:
Note: your getList function is not in this solution as the error could be with your API, JSON ect.
import SwiftUI
struct ContentView: View {
#ObservedObject var state = NewsState()
var body: some View {
Group { //needed for the IF Statement below
if state.stringList.count > 0 {
ForEach(self.state.stringList, id: \.self){ s in
Text(String(s))
}
}
}.onTapGesture {
self.state.getNewList()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class NewsState: ObservableObject {
#Published var stringList: Array<String> = []
init() {
self.getList()
}
func getList() {
self.stringList.append("New")
}
func getNewList() {
self.stringList = []
self.stringList.append("New new")
}
}

Related

ForEach in Scrollview does not get updated when #Published gets changed

My ForEach in a Scrollview does not get updated when CommentViewModel comments get updated. It gets successfully updated, but for some reason, CommentView does not get updated. I have tried everything, but can't seem to find a solution.
Maybe Comment should become a Hashable or Codable. But I can't quite make this work.
I also tried removing the chance of Scrollview being empty, by adding an if statement or empty Text. But this was not the problem.
Any help would be helpfull.
//These are the updated View
struct CommentView: View {
#StateObject var commentViewModel = CommentViewModel()
static let emptyScrollToString = "emptyScrollToString"
#State var commentCommentUser = ""
#State var showCommentComment = false
#State var post: Post
init(_ post: Post) {
self.post = post
}
var body: some View {
VStack {
commentView
Divider()
if showCommentComment {
HStack {
Text("Svarer \(commentCommentUser)")
.foregroundColor(.black)
.font(.system(size: 16))
.opacity(0.3)
Spacer()
Button {
withAnimation(Animation.spring().speed(2)) {
showCommentComment.toggle()
}
} label: {
Text("x")
.font(.system(size: 16))
.foregroundColor(.black)
}
}
.padding()
.background(Color(r: 237, g: 237, b: 237))
}
BottomBar(post: post)
.frame(minHeight: 50,maxHeight: 180)
.fixedSize(horizontal: false, vertical: true)
.shadow(radius: 60)
.navigationBarTitle("Kommentar", displayMode: .inline)
}
.onAppear() {
UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
commentViewModel.fetchComments(post: post)
}
}
private var commentView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
HStack{ Spacer() }
.id(Self.emptyScrollToString)
ForEach(commentViewModel.comments, id: \.id) { comment in
CommentCell(post: post, comment: comment, commentCommentUser: $commentCommentUser, showCommentComment: $showCommentComment)
}
}
.onReceive(Just(commentViewModel.comments.count)) { _ in // <-- here
withAnimation(.easeOut(duration: 0.5)) {
print("Scroll to top")
scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
}
}
}
}
}
public func uploadData(commentText: String) {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
guard let id = post.id else {return}
let data = ["fromId":uid, "commentText":commentText, "likes":0, "timestamp": Timestamp()] as [String : Any]
FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
.document().setData(data) { error in
if error != nil {
print("failed to post comment", error ?? "")
return
}
print("Update")
commentViewModel.fetchComments(post: post) //Gets error here
}
}
}
struct BottomBar: View {
var commentView: CommentView
init(post: Post) {
self.commentView = CommentView(post)
}
var body: some View {
bottomBar
}
private var bottomBar: some View {
HStack{
TextEditorView(string: $commentText)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(lineWidth: 1)
.opacity(0.5))
VStack {
Spacer()
Button {
commentView.uploadData() // This also reset all #State variables in Commentview, for some reason
commentText = ""
} label: {
Text("Slå op")
.font(.system(size: 20, weight: .semibold))
.opacity(commentText.isEmpty ? 0.5 : 1)
.foregroundColor(Color(r: 20, g: 147, b: 2))
}
.padding(.bottom, 10)
}
}
.padding()
}
}
struct Comment: Identifiable, Decodable {
#DocumentID var id: String?
let commentText: String
let fromId: String
var likes: Int
let timestamp: Timestamp
var user: PostUser?
var didLike: Bool? = false
}
class CommentViewModel: ObservableObject {
#Published var comments = [Comment]()
#Published var count = 0
let service: CommentService
let userService = UserService()
init(post: Post) {
self.service = CommentService(post: post)
fetchComments()
}
func fetchComments() {
service.fetchComments { comments in
self.comments = comments
self.count = self.comments.count
for i in 0 ..< comments.count {
let uid = comments[i].fromId
self.userService.fetchUser(withUid: uid) { user in
self.comments[i].user = user
}
}
}
}
}
struct CommentService {
let post: Post
func fetchComments(completion: #escaping([Comment]) -> Void) {
guard let id = post.id else {return}
FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
.order(by: "timestamp", descending: true)
.getDocuments { snapshot, error in
if error != nil {
print("failed fetching comments", error ?? "")
return
}
guard let docs = snapshot?.documents else {return}
do {
let comments = try docs.compactMap({ try $0.data(as: Comment.self) })
print("COmplete")
completion(comments)
}
catch {
print("failed")
}
}
}
}
This is the old views
struct Comment: Identifiable, Decodable {
#DocumentID var id: String?
let commentText: String
let fromId: String
var likes: Int
let timestamp: Timestamp
var user: PostUser?
var didLike: Bool? = false
}
class CommentViewModel: ObservableObject {
#Published var comments = [Comment]()
#Published var count = 0
let service: CommentService
let userService = UserService()
init(post: Post) {
self.service = CommentService(post: post)
fetchComments()
}
func fetchComments() {
service.fetchComments { comments in
self.comments = comments
self.count = self.comments.count
for i in 0 ..< comments.count {
let uid = comments[i].fromId
self.userService.fetchUser(withUid: uid) { user in
self.comments[i].user = user
}
}
}
}
}
struct CommentService {
let post: Post
func fetchComments(completion: #escaping([Comment]) -> Void) {
guard let id = post.id else {return}
FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
.order(by: "timestamp", descending: true)
.getDocuments { snapshot, error in
if error != nil {
print("failed fetching comments", error ?? "")
return
}
guard let docs = snapshot?.documents else {return}
do {
let comments = try docs.compactMap({ try $0.data(as: Comment.self) })
print("COmplete")
completion(comments)
}
catch {
print("failed")
}
}
}
}
struct CommentView: View {
#ObservedObject var commentViewModel: CommentViewModel
static let emptyScrollToString = "emptyScrollToString"
init(post: Post) {
commentViewModel = CommentViewModel(post: post)
}
var body: some View {
VStack {
commentView
Divider()
BottomBar(post: commentViewModel.service.post)
.frame(minHeight: 50,maxHeight: 180)
.fixedSize(horizontal: false, vertical: true)
.shadow(radius: 60)
.navigationBarTitle("Kommentar", displayMode: .inline)
}
.onAppear() {
UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
}
}
private var commentView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
HStack{ Spacer() }
.id(Self.emptyScrollToString)
ForEach(commentViewModel.comments, id: \.id) { comment in // Here should it update
let _ = print("Reload")
CommentCell(post: commentViewModel.service.post, comment: comment)
}
}
.onReceive(commentViewModel.$count) { _ in // It doesn't update here either
withAnimation(.easeOut(duration: 0.5)) {
print("Scroll to top")
scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
}
}
}
}
}
}
#StateObject property wrapper own the object you created, so it will keep alive once View updated by any changes.
#ObservedObject property wrapper doesn't own the object you created, so it will recreated on View update by any changes, in this way property observer get lost and will not be able to receive changes.
So, changing your ViewModel from #ObservedObject to #StateObject will fix the issue.
EDIT-1: Taking your new code into consideration.
Note, it is important to have a good grip on the SwiftUI basics, especially how to use and pass ObservableObject and how to use Views. I suggest you do the tutorial again.
I have attempted to modify your code to give you an idea on how you could re-structure it. Pay atttention to the details, hope it helps.
Note, I have commented a number of lines, because I do not have Firebase and your other code, such as UserService etc...
Adjust my code to suit your needs, and uncomment the relevant lines.
import Foundation
import SwiftUI
import Combine
struct BottomBar: View {
#ObservedObject var viewModel: CommentViewModel // <-- here
#State var post: Post
#State var commentText = ""
var body: some View {
HStack {
// TextEditorView(string: $commentText)
TextEditor(text: $commentText) // <-- for testing
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(lineWidth: 1)
.opacity(0.5))
VStack {
Spacer()
Button {
// -- here
viewModel.uploadData(post: post, commentText: commentText)
commentText = ""
} label: {
Text("Slå op")
.font(.system(size: 20, weight: .semibold))
.opacity(commentText.isEmpty ? 0.5 : 1)
}
.padding(.bottom, 10)
}
}
}
}
struct CommentView: View {
#StateObject var commentViewModel = CommentViewModel()
static let emptyScrollToString = "emptyScrollToString"
#State var commentCommentUser = ""
#State var showCommentComment = false
#State var post: Post
var body: some View {
VStack {
commentView
Divider()
if showCommentComment {
HStack {
Text("Svarer \(commentCommentUser)")
.foregroundColor(.black)
.font(.system(size: 16))
.opacity(0.3)
Spacer()
Button {
withAnimation(Animation.spring().speed(2)) {
showCommentComment.toggle()
}
} label: {
Text("x")
.font(.system(size: 16))
.foregroundColor(.black)
}
}.padding()
}
BottomBar(viewModel: commentViewModel, post: post)
.frame(minHeight: 50,maxHeight: 180)
.fixedSize(horizontal: false, vertical: true)
.shadow(radius: 60)
.navigationBarTitle("Kommentar", displayMode: .inline)
}
.onAppear() {
UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
commentViewModel.fetchComments(post: post)
}
}
private var commentView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
Spacer()
ForEach(commentViewModel.comments, id: \.id) { comment in
CommentCell(post: post, comment: comment, commentCommentUser: $commentCommentUser, showCommentComment: $showCommentComment)
}
}
.onReceive(Just(commentViewModel.comments.count)) { _ in // <-- here
withAnimation(.easeOut(duration: 0.5)) {
print("Scroll to top")
scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
}
}
}
}
}
}
// for testing
struct CommentCell: View {
#State var post: Post
#State var comment: Comment
#Binding var commentCommentUser: String
#Binding var showCommentComment: Bool
var body: some View {
Text(comment.commentText) // for testing
}
}
struct Comment: Identifiable, Decodable {
// #DocumentID
var id: String? // for testing
let commentText: String
let fromId: String
var likes: Int
// let timestamp: Timestamp // for testing
var user: PostUser?
var didLike: Bool? = false
}
class CommentViewModel: ObservableObject {
#Published var comments = [Comment]()
let service = CommentService() // <-- here
// let userService = UserService() // for testing
init() { } // <-- here
func fetchComments(post: Post) { // <-- here
service.fetchComments(post: post) { comments in
self.comments = comments
for i in 0 ..< comments.count {
let uid = comments[i].fromId
// self.userService.fetchUser(withUid: uid) { user in
// self.comments[i].user = user
// }
}
}
}
func uploadData(post: Post, commentText: String) {
service.uploadData(post: post, commentText: commentText) { isGood in
if isGood {
self.fetchComments(post: post)
}
}
}
}
struct CommentService {
func fetchComments(post: Post, completion: #escaping([Comment]) -> Void) {
guard let id = post.id else {completion([]); return} // <-- here
// FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
// .order(by: "timestamp", descending: true)
// .getDocuments { snapshot, error in
// if error != nil {
// print("failed fetching comments", error ?? "")
// return
// }
// guard let docs = snapshot?.documents else {return}
// do {
// let comments = try docs.compactMap({ try $0.data(as: Comment.self) })
// print("COmplete")
// completion(comments)
// }
// catch {
// print("failed")
// completion([])
// }
// }
}
func uploadData(post: Post, commentText: String, completion: #escaping(Bool) -> Void) {
completion(true) // for testing, to be removed
// guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {completion(false); return} // <--- here
// guard let id = post.id else {completion(false); return} // <--- here
//
// let data = ["fromId":uid, "commentText":commentText, "likes":0, "timestamp": Timestamp()] as [String : Any]
// FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
// .document().setData(data) { error in
// if error != nil {
// print("failed to post comment", error ?? "")
// completion(false) // <--- here
// return
// }
// print("Update")
// completion(true) // <--- here
// }
}
}
// for testing
struct PostUser: Identifiable, Decodable {
var id: String?
}
// for testing
struct Post: Identifiable, Decodable {
var id: String?
var name = "something"
}
EDIT-2: typo fix.
in BottomBar changed viewModel.service.uploadData(post: post, commentText: commentText) {_ in}
to viewModel.uploadData(post: post, commentText: commentText)

get the right index onTapGesture with AsyncImage

While iterating over images and loading AsyncImages, the .onTapGesture does not refer to the clicked element.
Is this due to View refresh on image loading? How to bypass this issue?
var images: [String] = [
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
"https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]
struct thumbnail: View {
#State var mainImageUrl: String = images[0];
var body: some View {
VStack {
AsyncImage(url: URL(string: mainImageUrl)) { image in
image
.resizable().scaledToFit().frame(height: 350)
} placeholder: {
ProgressView()
}.frame(height: 350).cornerRadius(10)
HStack {
ForEach(images, id: \.self) { imageUrl in
AsyncImage(url: URL(string: imageUrl)) { sourceImage in
sourceImage
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100, alignment: .center)
.clipped()
} placeholder: {
ProgressView()
}.onTapGesture {
self.mainImageUrl = imageUrl
}
}
}
}
}
}
It sort of works if you just flip the frame and aspectRatio as in the code below.
However it is very slow and you are constantly re-downloading the images whenever you click on a thumbnail.
The last image is specially slow.
struct ContentView: View {
var body: some View {
Thumbnail()
}
}
struct Thumbnail: View {
var images: [String] = [
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
"https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]
#State var mainImageUrl: String = "https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg"
var body: some View {
VStack {
AsyncImage(url: URL(string: mainImageUrl)) { image in
image.resizable().scaledToFit().frame(height: 350)
} placeholder: {
ProgressView()
}.frame(height: 350).cornerRadius(10)
HStack {
ForEach(images, id: \.self) { imageUrl in
AsyncImage(url: URL(string: imageUrl)) { sourceImage in
sourceImage
.resizable()
.frame(width: 100, height: 100) // <--- here
.aspectRatio(contentMode: .fill) // <--- here
.clipped()
} placeholder: {
ProgressView()
}.onTapGesture {
self.mainImageUrl = imageUrl
}
}
}
}
}
}
IMHO, a better way is to use a different approach to avoid the constant downloading of images.
You could download the pictures in parallel only once, using swift async/await concurrency.
Such as in this code:
struct Thumbnail: View {
#StateObject var loader = ImageLoader()
#State var selectedPhoto: PhotoImg?
var body: some View {
VStack {
if loader.images.count < 1 {
ProgressView()
} else {
Image(uiImage: selectedPhoto?.image ?? UIImage(systemName: "smiley")!)
.frame(height: 350).cornerRadius(10)
ScrollView {
HStack (spacing: 10) {
ForEach(loader.images) { photo in
Image(uiImage: photo.image)
.resizable()
.frame(width: 100, height: 100)
.aspectRatio(contentMode: .fill)
.onTapGesture {
selectedPhoto = photo
}
}
}
}
.onAppear {
if let first = loader.images.first {
selectedPhoto = first
}
}
}
}
.task {
await loader.loadParallel()
}
}
}
class ImageLoader: ObservableObject {
#Published var images: [PhotoImg] = []
let urls: [String] = [
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
"https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
"https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]
func loadParallel() async {
return await withTaskGroup(of: (String, UIImage).self) { group in
for str in urls {
if let url = URL(string: str) {
group.addTask { await (url.absoluteString, self.loadImage(url: url)) }
}
}
for await result in group {
DispatchQueue.main.async {
self.images.append(PhotoImg(url: result.0, image: result.1))
}
}
}
}
private func loadImage(url: URL) async -> UIImage {
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let img = UIImage(data: data) { return img }
}
catch { print(error) }
return UIImage()
}
}
struct PhotoImg: Identifiable, Hashable {
let id = UUID()
var url: String
var image: UIImage
}
This was very useful. Thanks. I simply had to lines flipped:
.scaledToFill()
.frame(width: 80, height: 80)
Yet it seemed those two lines in the order presented above caused this to work properly. Any idea why this is the case?

How to loop through API call data and display in view

I know I can use for each for this, but every time I try to implement according to documentation it throws some kind of error regarding syntax.
Here is my view:
import SwiftUI
import Combine
struct HomeTab: View {
#StateObject var callDevices = CallDevices()
var body: some View {
NavigationView {
devices
.onAppear {
callDevices.getDevices()
}
}
}
private var devices: some View {
VStack(alignment: .leading, spacing: nil) {
ForEach(content: callDevices.getDevices(), id: \.self) { device in
// i want to loop through and display here //
HStack{
Text(device.Name)
Text(device.Status)
}
}
Spacer()
}
}
}
struct HomeTab_Previews: PreviewProvider {
static var previews: some View {
HomeTab()
}
}
Here is my Call Devices which works without issue in other views:
class CallDevices: ObservableObject {
private var project_id: String = "r32fddsf"
#Published var devices = [Device]()
func getDevices() {
guard let url = URL(string: "www.example.com") else {return}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Authorization")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
// guard let data = data else {print("empty data"); return }
let theData = try! JSONDecoder().decode(Welcome.self, from: data!)
DispatchQueue.main.async {
self.devices = theData.devices
}
}
.resume()
}
}
is the issue in the way I am calling my function?
try this:
(you may need to make Device Hashable)
private var devices: some View {
VStack(alignment: .leading, spacing: nil) {
ForEach(callDevices.devices, id: \.self) { device in // <-- here
// i want to loop through and display here //
HStack{
Text(device.Name)
Text(device.Status)
}
}
Spacer()
}
}
If Device is Identifiable, you can remove the id: \.self.
struct Device: Identifiable, Hashable, Codable {
let id = UUID()
var Name = ""
var Status = ""
// ... any other stuff you have
}

Gesture of child View not working in SwiftUI

Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to make two simultaneous gestures work in SwiftUI.
The child-View at question is a VideoPlayer.
The parent-View is a ZStack.
I am using the .simultaneousGesture modifier, hoping that this will allow all child-View gestures to still go through.
But they don't !
The following code works but does not allow the VideoPlayer's control gestures to be recognised.
In fact, the restartScannerTapGesture TapGesture in my example does always kick in - and the VideoPlayer control gestures (like pause, play, stop etc.) are unfortunately ignored.
Any idea on how to get child-View gestures work when added a gesture to a parent-View in Swift UI ??
struct ParentView: View {
#EnvironmentObject var myURLList
var body: some View {
let restartScannerTapGesture = TapGesture(count: 1)
.onEnded {
actionClickID = UUID()
}
ZStack {
if let url = URL(fileURLWithPath: myURLList[0].path){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
.onAppear() {
isVideo = false
}
} else if url.containsVideo {
CustomPlayerView(url: url, isVideo: $isVideo)
.onAppear() {
isVideo = true
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
}
}
.simultaneousGesture(restartScannerTapGesture)
}
And here is the CustomPlayerView that shows the Video controls in the first place.
It clearly is the child-View at question. Its gestures should also work, but they don't. Why ??
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
func playVideo() {
avPlayer?.play()
}
func stopVideo() {
avPlayer?.pause()
avPlayer?.replaceCurrentItem(with: nil)
}
}
struct CustomPlayerView: View {
var url : URL
#Binding var isVideo: Bool
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
.onAppear() {
isVideo = false
}
} else if url.containsVideo {
CustomPlayerView(url: url, isVideo: $isVideo)
.onAppear() {
isVideo = true
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
.onAppear() {
isVideo = false
}
}
}
}
And the needed extension:
extension URL {
func mimeType() -> String {
let pathExtension = self.pathExtension
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
return "application/octet-stream"
}
var containsImage: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeImage)
}
var containsAudio: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeAudio)
}
var containsVideo: Bool {
let mimeType = self.mimeType()
guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)?.takeRetainedValue() else {
return false
}
return UTTypeConformsTo(uti, kUTTypeMovie)
}
}

Display filename in next View too

I have a code that makes a http Request, gets an array with filenames from that, displays them each with an image and the filename below. Everything works fine.
Now I made each image a button that opens a detail page.
That works but at the top it should say the matching filename from the page before.
But I am not able to hand over the filename (name) from ContentView4 to the next page (ts).
The language is SwiftUi
Could you please help me?
Thanks
Nikias
Here is my code:
import SwiftUI
struct ContentView4: View {
#State var showingDetail = false
#State var username: String = "."
#State var password: String = "."
#State private var name = String("Nikias2")
#State private var t = String()
#State private var x = -1
#State var dateien = ["word.png"]
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
$showingDetail) {
ts(name: t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
doHttpRequest()
}
}
}
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
ContentView4()
}
}
struct ts: View {
#State var hin = false
#State var um = false
#State var datname: String = ""
var name: String
var body: some View {
NavigationView {
VStack {
Text(name)
.font(.system(size: 60))
.foregroundColor(.black)
.padding(50)
Button(action: {
self.hin.toggle()
}) {
Text("+")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.yellow)
.cornerRadius(35.0)
}
.padding()
if hin {
HStack {
Text("Datei auswählen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei hochladen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei herunterladen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.blue)
.cornerRadius(35.0)
Button(action: {
self.um.toggle()
}) {
Text("Datei umbenennen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(35.0)
}
.padding()
if um {
HStack {
TextField(name, text: $datname)
.font(.headline)
.frame(width: 150, height: 70)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei umbenennen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.green)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei löschen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.red)
.cornerRadius(35.0)
}
}
}
}
I believe your issue is a result of using #State variables to store all of the attributes. #State variables are not consistent and get refreshed in the background by SwiftUI depending on your views visibility.
The piece that you are missing is a view controller class stored in an #EnviornmentObject variable. This class gets Initiated in your main contentView and is used to keep track and alter of all your attributes.
Each ContentView should reference the single #EnviornmentObject and pull data from that class.
Another solution which may work would be to replace all your #State variables with #StateObject vars. #StateObject vars are basically #State vars but get initiated before the struct get loaded and the value is kept consistent regardless of the view state of the parent struct.
Here is a rough implementation of #EnvironmentObject within your project.
Basically use the #EnvironmentObject to pass values to child views
ContentView4.swift
struct ContentView4: View {
#EnvironmentObject cv4Controller: ContentView4Controller
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< cv4Controller.dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = cv4Controller.dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(cv4Controller.t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
cv4Controller.$showingDetail) {
ts(name: cv4Controller.t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
cv4Controller.doHttpRequest()
}
}
}
ContentView4Controller.swift
class ContentView4Controller: ObservableObject {
#Published var showingDetail = false
#Published var username: String = "."
#Published var password: String = "."
#Published private var name = String("Nikias2")
#Published private var t = String()
#Published private var x = -1
#Published private var t = String()
#Published private var x = -1
#Published var dateien = ["word.png"]
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
Example of main ContentView.swift
struct ContentView: View {
var cv4Controller: ContentView4Controller = ContentView4Controller()
var body: some view {
// your main page output
GeometryReader { geo in
// just a guess for what you have in your main contentView
switch(page) {
case .main:
ContentView2()
default:
ContentView4()
break
}
}.environmentObject(cv4Controller) // this will make cv4Controller available to all child view structs
}
}
Add #Binding wrapper to the "name" variable in your ts view. And pass the t variable as a binding by adding a "$". This will keep your ts name variable updated to whatever is value it has in the parent view.
Also why do you use a NavigationView in your ts View?
struct ContentView4: View {
...
#State private var t = String()
...
var body: some View {
...
ZStack{
...
}
.sheet(isPresented: $showingDetail) {
ts(name: $t)
}
...
}
func doHttpRequest() {
...
}
}
struct ts: View {
...
#Binding var name: String
var body: some View {
...
}
}
My starting code works, but It's just displaying the Filenames in a row and if I tap a random image, the name won't fit, only if I'm going down in the row and tap them. The problem is, that I don't know how to set the variable to the id, not to pass them to the next view. Has anyone got and idea how I can pass the right filename into a variable in the for loop and read it in the next view?