I have a problem with displaying the data inside the sheet - ios

I am trying to display a link from the variable
#StateObject var modelNew = Api()
But I want to take a value in the sheet
Inside the view, I can view the entire content without a problem
struct detiles2: View {
var model : model
#State var isPlayer = false
#StateObject var modelNew = Api()
var body: some View {
VStack {
let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
URLImage(link!) { image in
image
.resizable()
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
.frame(maxWidth: .infinity, maxHeight: 200)
}
Text("Hello").padding()
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(modelNew.models) { item in
HStack {
ZStack {
Circle()
.frame(maxWidth: 30, maxHeight: 30)
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
Image(systemName: "play.fill")
.foregroundColor(Color(UIColor.systemBackground))
.onTapGesture {
isPlayer.toggle()
}
}.padding(.leading, 15)
Spacer()
VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
Text(item.algiment).font(.footnote).foregroundColor(Color.brown)
}.padding(.trailing, 5)
let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
URLImage(link!) { image in
image
.resizable()
.cornerRadius(20)
.frame(maxWidth: 80, maxHeight: 80)
}
}.padding(.trailing, 5)
}
}
}
Spacer()
}
.onAppear() {
modelNew.getData(url: model.url!)
}
.sheet(isPresented: $isPlayer) {
player(url: <String>) //Here ----- I want to pull it out like right away ForEach(modelNew.models) { item in} To get item.url
}
.ignoresSafeArea()
}
}
Here I am fetching new data and it was displayed correctly in the view, but inside the sheet I was not able to do so
class Api : ObservableObject{
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
print(data)
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}

you could try a different approach using .sheet(item:...) as in
this sample code, to show your sheet with the video player. You should be able to adapt this code to suit your purpose. Works for me.
import Foundation
import AVFoundation
import AVKit
import SwiftUI
struct SiteURL: Identifiable {
let id = UUID()
var urlString: String
}
struct ContentView: View {
#State var sheetUrl: SiteURL?
var body: some View {
Button("show player sheet", action: {
sheetUrl = SiteURL(urlString: "https://jplayer.org/video/m4v/Finding_Nemo_Teaser.m4v")
})
.sheet(item: $sheetUrl) { site in
if let url = URL(string: site.urlString) {
VideoPlayer(player: AVPlayer(url: url))
}
}
}
}
So, in your .onTapGesture {...} set the sheetUrl with your item info, like in the sample code Button,
instead of isPlayer.toggle().
EDIT-1: here is another example code to show that my answer with .sheet(item:...) works.
struct Model: Identifiable, Codable {
let id = UUID()
var image_path: String
var title: String
var algiment: String
var url: String
}
class Api: ObservableObject {
// for testing
#Published var models: [Model] = [
Model(image_path: "image_path-1", title: "Finding_Nemo_Teaser", algiment: "model algiment-1", url: "https://jplayer.org/video/m4v/Finding_Nemo_Teaser.m4v"),
Model(image_path: "image_path-2", title: "Incredibles_Teaser", algiment: "model algiment-2", url: "https://jplayer.org/video/m4v/Incredibles_Teaser.m4v")]
func getData(url: String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
print(data)
do {
let dataModel = try JSONDecoder().decode([Model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
struct TestView: View {
#State var videoUrl: SiteURL? // <-- here
// var model : model
#StateObject var modelNew = Api()
var body: some View {
VStack {
// let link = URL(string: "https://ccc/" + (model.image_path ?? ""))
// URLImage(link!) { image in
// image
// .resizable()
// .shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
// .frame(maxWidth: .infinity, maxHeight: 200)
// }
Text("Hello").padding()
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(modelNew.models) { item in
HStack {
ZStack {
Circle()
.frame(maxWidth: 30, maxHeight: 30)
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
Image(systemName: "play.fill")
.foregroundColor(Color(UIColor.systemBackground))
.onTapGesture {
videoUrl = SiteURL(urlString: item.url) // <-- here
}
}.padding(.leading, 15)
Spacer()
VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
Text(item.algiment).font(.footnote).foregroundColor(Color.brown)
}.padding(.trailing, 5)
// let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
// URLImage(link!) { image in
// image
// .resizable()
// .cornerRadius(20)
// .frame(maxWidth: 80, maxHeight: 80)
// }
}.padding(.trailing, 5)
}
}
}
Spacer()
}
.onAppear() {
// modelNew.getData(url: model.url!)
}
.sheet(item: $videoUrl) { site in // <-- here
if let url = URL(string: site.urlString) {
VideoPlayer(player: AVPlayer(url: url))
}
}
.ignoresSafeArea()
}
}
struct ContentView: View {
var body: some View {
TestView()
}
}

Related

SwiftUi how can I load selected video from item identifier

I am new to Swift and have been using the new PhotosPicker in SwiftUI 4.0 . I am able to display selected images but not video . When I select a video I can get a few pieces of information like this below
PhotosPickerItem(_itemIdentifier: Optional("40F724uF-24M7-4523-9B2B-AD43FB2C7D71/L0/001"), _shouldExposeItemIdentifier: false, _supportedContentTypes: [<_UTCoreType 0x106c4cd60> com.apple.quicktime-movie (not dynamic, declared)], _itemProvider: <PUPhotosFileProviderItemProvider: 0x600003ea05a0> {types = (
"com.apple.quicktime-movie"
)})
I am wondering if there is somehow that I can use that item identifier to load the video selected . I am been looking at different examples but none of them show videos for PhotosPicker . I have a very small project that I am testing this on, any suggestions would be great
import SwiftUI
import Combine
import PhotosUI
import AVKit
struct PlayVideoView: View {
#State private var selectedItem: [PhotosPickerItem] = []
#State private var data: Data?
#State var player = AVPlayer(url: URL(string: "https://swiftanytime-content.s3.ap-south-1.amazonaws.com/SwiftUI-Beginner/Video-Player/iMacAdvertisement.mp4")!)
var body: some View {
PhotosPicker(selection: $selectedItem,
maxSelectionCount: 1,
matching: .any(of: [.images,.videos])) {
Image(systemName: "photo")
.resizable()
.foregroundColor(.blue)
.frame(width: 24, height: 24)
.padding(.top, 5.0)
}.onChange(of: selectedItem) { newMedia in
Task {
guard let item = selectedItem.first else {
return
}
item.loadTransferable(type: Data.self) { result in
switch result {
case .success(let data):
if let data = data {
print(item) // get video url here and display in videoplayer
self.data = data
} else {
print("data is nil")
}
case .failure(let failure):
fatalError("\(failure)")
}
}
}
}
VideoPlayer(player: player) // selected video url should go here
.frame(width: 400, height: 300, alignment: .center)
}
}
struct PlayVideoView_Previews: PreviewProvider {
static var previews: some View {
PlayVideoView()
}
}
You could try this approach, using the code from https://github.com/zunda-pixel/SamplePhotosPicker.
The Movie code replicated here, uses the TransferRepresentation to represent
a url from the temp file.
import Foundation
import SwiftUI
import PhotosUI
import AVKit
import CoreTransferable
// from: https://github.com/zunda-pixel/SamplePhotosPicker
struct Movie: Transferable {
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) { movie in
SentTransferredFile(movie.url)
} importing: { receivedData in
let fileName = receivedData.file.lastPathComponent
let copy: URL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: copy.path) {
try FileManager.default.removeItem(at: copy)
}
try FileManager.default.copyItem(at: receivedData.file, to: copy)
return .init(url: copy)
}
}
}
struct ContentView: View {
var body: some View {
PlayVideoView()
}
}
struct PlayVideoView: View {
#State private var selectedItem: [PhotosPickerItem] = []
#State var player = AVPlayer(url: URL(string: "https://swiftanytime-content.s3.ap-south-1.amazonaws.com/SwiftUI-Beginner/Video-Player/iMacAdvertisement.mp4")!)
var body: some View {
PhotosPicker(selection: $selectedItem,
maxSelectionCount: 1,
matching: .any(of: [.images,.videos])) {
Image(systemName: "photo")
.resizable()
.foregroundColor(.blue)
.frame(width: 24, height: 24)
.padding(.top, 5.0)
}.onChange(of: selectedItem) { newMedia in
Task {
guard let item = selectedItem.first else { return }
item.loadTransferable(type: Movie.self) { result in // <-- here
switch result {
case .success(let movie):
if let movie = movie {
player = AVPlayer(url: movie.url) // <-- here
} else {
print("movie is nil")
}
case .failure(let failure):
fatalError("\(failure)")
}
}
}
}
VideoPlayer(player: player)
.frame(width: 400, height: 300, alignment: .center)
}
}

Issues with assigning URL to an optional URL State variable

I am trying to assign variable "furl" to #State var fileurl: URL? in saveImage() of my MainScreenView(). In saveImage Print(furl) shows the URL, but after assigning fileURL = furl, print(fileurl) returns nil, that's the issue. Removing the optional doesn't allow it to build as:
if let furl = fileurl {
let data = try Data(contentsOf: furl)
if let img = UIImage(data: data) {
return img
}.
of loadImage()Throws an error of "Initializer for conditional binding must have Optional type, not 'URL'". I'm unsure what to do in this case.
Here's MainScreenView
import SwiftUI
import SafariServices
struct MainScreenView: View {
#State var isfound = true
#State var failed = false
#State var ShowInstruction = false
#State var fileurl: URL? //I'm guessing since this is an optional, it's not allow it to do something correctly?
#State var ShowBadge = false
#State var ShowPortal = false
#State var ShowDetails = false
#State var alert = false
#State var listalert = false
#State var truebadge: Bool = false
var body: some View {
NavigationView {
Color("BackgroundMain")
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
Button {
ShowBadge = true
} label: {
if truebadge != true{
VStack{
Image(systemName: "person")
.font(.system(size: 120))
.foregroundColor(Color.white)
.padding()
Text("expired compliance")
.font(.system(size: 30))
.foregroundColor(Color.white)
}.offset(y: -30)
}
else {
VStack{
Image(systemName: "person")
.font(.system(size: 120))
.foregroundColor(Color("MainBadgeScreen"))
.padding()
Text("Show Complaince")
.font(.system(size: 30))
.foregroundColor(Color("MainBadgeScreen"))
}.offset(y: -30)
}
}
Button {
} label: {
HStack {
Image(systemName: "lanyardcard")
Text("Student ID")
}
.font(.system(size: 23))
.foregroundColor(.white)
.frame(width: 200, height: 65, alignment: .center)
.background(Color.blue)
.cornerRadius(15)
.offset(y: -10)
}
Button {
listalert.toggle()
} label: {
HStack{
Image(systemName: "gear.circle")
Text("Settings")
}
.font(.system(size: 23))
.foregroundColor(.white)
.frame(width: 200, height: 65, alignment: .center)
.background(Color.gray)
.cornerRadius(15)
}
}
).sheet(isPresented: $listalert, content: {
NavigationView{
List {
Button(action: {
ShowPortal = true
}) {
HStack{
Text("Student Health Portal")
Spacer()
Image(systemName: "heart.fill")
}.foregroundColor(Color.red)
.font(.system(size: 20))}
Button(action: {
ShowInstruction = true
}) {
HStack{
Text("Instructions")
Spacer()
Image(systemName: "questionmark.circle")
}.font(.system(size: 20))}
Button(action: {
ShowDetails = true
}) {
HStack{
Text("About")
Spacer()
Image(systemName: "info.circle")
}.font(.system(size: 20))}
}
.navigationBarTitle(Text("Settings"), displayMode: .inline)
}
.sheet(isPresented: $ShowPortal, content: {
safari()
})
.sheet(isPresented: $ShowInstruction, content: {
Instruction()
})
.sheet(isPresented: $ShowDetails, content: {
Details()
})
})
.sheet(isPresented: $ShowBadge, content: {
BadgeScreenView(complianceview: loadImage(), truebadge: truebadge)
})
.navigationTitle("Home")
.navigationBarTitleDisplayMode(.large)
}
}
func saveImage(binding: Binding<Bool>, image: UIImage){
//Need to set #State var truebadge to 'true'
let finalcompliance = image.self.pngData()
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("Compliance")
.appendingPathExtension("png")
print(furl) // shows url from furl
fileurl = furl // assigning furl to fileurl allows it to be assigned. But ends up becoming nil
print(fileurl) //nil value; doesn't allow the image to load in loadimage() even though assigned to furl
try finalcompliance?.write(to: furl)
} catch{
print("could not create imageFile")
}
}
func loadImage() -> UIImage {
do {
print("showing the image!")
if let furl = fileurl {
let data = try Data(contentsOf: furl)
if let img = UIImage(data: data) {
return img
}
}
} catch {
print("error: \(error)") // todo
}
return UIImage()
}
}
struct safari : UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<safari>) -> SFSafariViewController{
let controller = SFSafariViewController(url: URL(string: "https://patientportal.bowiestate.edu/login_directory.aspx")!)
return controller
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<safari>) {
}
}

Why does my array get cleared out when observedobject gets updated?

I'm new to SwiftUI and MVVM and have been working on a podcast app and can't figure out for the life of me how to resolve this issue.
I have a list of podcast episodes with a button assigned to each in a VStack, when pressed, updates the userStore and presents a minimized player. When this happens, my list disappears and I get the ActivityIndicator and the list never reappears. I'm guessing the array gets cleared out whenever the state is updated. I don't want this behavior. What am I doing wrong?
struct PodcastDetailView: View {
#EnvironmentObject var userStore: UserStore
#ObservedObject var minimizableViewHandler: MinimizableViewHandler
#ObservedObject var player: Player = Container.player
#ObservedObject var podcastViewModel: PodcastViewModel
init(podcast: Podcast, player: Player = Container.player, minimizableViewHandler: MinimizableViewHandler) {
self.podcastViewModel = PodcastViewModel(podcast: podcast)
self.player = player
self.minimizableViewHandler = minimizableViewHandler
}
var body: some View {
ZStack{
Color(hex: "1B1D26")
.edgesIgnoringSafeArea([.all])
VStack(alignment: .leading, spacing: 10) {
PodcastDetailHeader(podcast: podcastViewModel.podcast)
if podcastViewModel.episodes.isEmpty {
ActivityIndicator()
.frame(width: 120, height: 120)
.foregroundColor(Color(hex: "813F97"))
.opacity(0.8)
.animation(.easeOut)
} else {
ScrollView {
VStack(alignment: .center, spacing: 10)
{
ForEach(podcastViewModel.episodes, id: \.self) { episode in
Button(action: {
if (self.player.state == .empty) {
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.minimizableViewHandler.present()
} else {
// item could not be found
}
} else {
print("new episode is " + episode.title)
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.player.play()
}
}
}) {
PodcastRowView(episode: episode)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 8)
}.buttonStyle(PlainButtonStyle())
.padding(.leading, 20)
.padding(.trailing, 10)
}
}
}
}
Spacer()
}
}
.navigationBarBackButtonHidden(true)
.navigationBarTitle(Text(self.podcastViewModel.podcast.title), displayMode: .inline)
.onAppear {
print("appearing")
self.podcastViewModel.loadEpisodes()
}
}
import Combine
import SwiftUI
class PodcastViewModel: ObservableObject {
private let apiService: APIService
private var episodesCancelable: Cancellable?
#Published var podcast: Podcast
#Published var episodes: [Episode] = []
init(podcast: Podcast, apiService: APIService = APIService()) {
self.podcast = podcast
self.apiService = apiService
}
deinit {
episodesCancelable?.cancel()
}
func loadEpisodes() {
episodesCancelable = apiService.episodes(for: podcast)
.receive(on: RunLoop.main)
.replaceError(with: [])
.assign(to: \.episodes, on: self)
}
}
I took FarouK's advice and used #StateObject. I had to change a couple of things but got it working.
struct PodcastDetailView: View {
#EnvironmentObject var userStore: UserStore
#ObservedObject var minimizableViewHandler: MinimizableViewHandler
#ObservedObject var player: Player = Container.player
#StateObject var podcastViewModel: PodcastViewModel
var podcast: Podcast
var body: some View {
ZStack{
Color(hex: "1B1D26")
.edgesIgnoringSafeArea([.all])
VStack(alignment: .leading, spacing: 10) {
PodcastDetailHeader(podcast: podcastViewModel.podcast)
if podcastViewModel.episodes.isEmpty {
ActivityIndicator()
.frame(width: 120, height: 120)
.foregroundColor(Color(hex: "813F97"))
.opacity(0.8)
.animation(.easeOut)
} else {
ScrollView {
VStack(alignment: .center, spacing: 10)
{
ForEach(podcastViewModel.episodes, id: \.self) { episode in
Button(action: {
if (self.player.state == .empty) {
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.minimizableViewHandler.present()
} else {
// item could not be found
}
} else {
print("new episode is " + episode.title)
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.player.play()
}
}
}) {
PodcastRowView(episode: episode)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 8)
}.buttonStyle(PlainButtonStyle())
.padding(.leading, 20)
.padding(.trailing, 10)
}
}
}
}
Spacer()
}
}
.navigationBarBackButtonHidden(true)
.navigationBarTitle(Text(self.podcastViewModel.podcast.title), displayMode: .inline)
.onAppear {
self.podcastViewModel.podcast = self.podcast
self.podcastViewModel.loadEpisodes()
}
}
import Combine
import SwiftUI
class PodcastViewModel: ObservableObject {
private let apiService: APIService
private var episodesCancelable: Cancellable?
#Published var podcast: Podcast = Podcast(id: "", title: "", image: URL(string: ""), thumbnail: URL(string: ""), totalEpisodes: 0, explicitContent: true, description: "", language: "", country: "", rss: URL(string: ""), latestPubDateMs: Date(), earliestPubDateMs: Date(), publisher: "")
#Published var episodes: [Episode] = []
init(apiService: APIService = APIService()) {
self.apiService = apiService
}
deinit {
episodesCancelable?.cancel()
}
func loadEpisodes() {
episodesCancelable = apiService.episodes(for: podcast)
.receive(on: RunLoop.main)
.replaceError(with: [])
.assign(to: \.episodes, on: self)
}
}

Have 0th item in SwiftUI auto selected

I have data loaded into an HStack that is in a Scroll View in SwiftUI. Right now I have it coded where a user can tap on one of those items and have it selected. I'd like for the 0th item to already be selected upon load.
import SwiftUI
import Combine
import Contentful
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil
#Binding var show: Bool
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectMovie: self.$selectMovie)
}
.onAppear() {
self.selectMovie = self.movies.count > 0 ? self.movies.first! : nil
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
}
}
}
}
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie?
#State var initialImage = UIImage()
var body: some View {
ZStack(alignment: .center) {
if movie == selectedMovie {
Image("")
.resizable()
.frame(width: 187, height: 254)
.overlay(
RoundedRectangle(cornerRadius: 13)
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 182, height: 249)
.onAppear {
let urlString = "\(urlBase)\(self.movie.movieId).png?"
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
} else {
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 135, height: 179)
.onAppear {
let urlString = "\(urlBase)\(self.movie.movieId).png?"
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
}
.onTapGesture {
self.selectedMovie = self.movie
}
}
}
EDIT:
I've added the below suggestion but it's still now working properly. Maybe it's where I added the .onAppear?
So when I launch my app I see the 0th item is selected but when I tap on any item the view just reloads but the 0th item always stays selected.
Additional issue:
Also, my #ObservedObject var fetcher = MovieFetcher() in moviesView is called repeatedly.
Since you haven't given the full working code, I wasn't able to reproduce the issue you've mentioned. However, I'd suggest you move the .onAppear from ForEach to the HStack (see code below).
I couldn't reproduce the issue you specified.
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectedMovie: self.$selectMovie)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
.onAppear() {
self.selectMovie = self.fetcher.movies.count > 0 ? self.fetcher.movies.first! : nil
}
}
}
}
In the struct moviesView, use the below code to auto select the first movie.
.onAppear() {
self.selectMovie = self.movies.count > 0 ? self.movies.first! : nil
}
Let me know if you have any other questions.

How to refer to #Published var within: class NetworkManager: ObservableObject

I need to define testData:[Test] that refers to #Published var tests:[Test] within
class NetworkManager: ObservableObject (see code).
I have tried the following definition:
/// The app does not compile with this definition
//let testData:[Test] = NetworkManager(tests: Test)
/// The app works with this definition, but shows no remote json data
let testData:[Test] = NetworkManager().tests
class NetworkManager: ObservableObject {
#Published var tests:[Test] = [Test]()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://my-url/remote.json")!)
let task = URLSession.shared.dataTask(with: file) { (data, _, error) in
guard error == nil else { return }
do {
let tests = try JSONDecoder().decode([Test].self, from: data!)
DispatchQueue.main.async {
self.tests = tests
print(tests)
}
} catch {
print("Failed To decode: ", error)
}
}
task.resume()
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
The code below works fine
/// The app works with this definition and shows the local json data
let testData:[Test] = load("local.json")
func load<T:Decodable>(_ filename:String, as type:T.Type = T.self) -> T {
let data:Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
However, for the first part I get the error message:
"Cannot convert value of type 'Test.Type' to expected argument type '[Test]'"
What am I missing here? Any help is highly appreciated.
Additional info in response to the answer and question that shows how testData is used:
import SwiftUI
import Combine
struct Test: Hashable, Codable, Identifiable {
var id:Int
var imageName:String
var imageUrl:String
var category:Category
var description:String
enum Category: String, CaseIterable, Codable, Hashable {
case t1 = "test1"
case t2 = "test2"
case t3 = "test3"
}
}
class NetworkManager: ObservableObject {
#Published var tests:[Test] = [Test]()
private var subscriptions = Set<AnyCancellable>()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://my-url/remote.json")!)
URLSession
.shared
.dataTaskPublisher(for: file)
.map(\.data)
.decode(type: [Test].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.assign(to: \.tests, on: self)
.store(in: &subscriptions)
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
let testData:[Test] = NetworkManager().tests
struct ContentView: View {
var categories:[String:[Test]] {
.init(
grouping: testData,
by: {$0.category.rawValue}
)
}
var body: some View {
NavigationView{
List (categories.keys.sorted(), id: \String.self) {key in TestRow(categoryName: "\(key) - Case".uppercased(), tests: self.categories[key]!)
.frame(height: 320)
.padding(.top)
.padding(.bottom)
}
.navigationBarTitle(Text("TEST"))
}
}
}
struct TestRow: View {
var categoryName:String
var tests:[Test]
var body: some View {
VStack {
Text(self.categoryName)
.font(.title)
.multilineTextAlignment(.leading)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach(self.tests, id: \.self) { tests in
NavigationLink(destination:
TestDetail(test: tests)) {
TestItem(test: tests)
.frame(width: 300)
.padding(.trailing, 30)
Spacer()
}}
}
.padding(.leading)
}
}
}
}
struct TestDetail: View {
var test:Test
var body: some View {
List{
ZStack(alignment: .bottom) {
Image(test.imageUrl)
.resizable()
.aspectRatio(contentMode: .fit)
Rectangle()
.padding()
.frame(height: 80.0)
.opacity(0.25)
.blur(radius: 10)
HStack{
VStack(alignment: .leading) {
Text(test.imageName)
.padding()
// .color(.white)
.colorScheme(.light)
.font(.largeTitle)
}
.padding(.leading)
.padding(.bottom)
Spacer()
}
}
.listRowInsets(EdgeInsets())
VStack(alignment: .leading) {
Text(test.description)
// .padding(.bottom)
// .color(.primary)
.colorScheme(.light)
.font(.body)
.lineLimit(nil)
.lineSpacing(12)
HStack {
Spacer()
OrderButton()
Spacer()
}.padding(.top, 50)
}.padding(.top)
.padding(.bottom)
}
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(true)
}
}
struct TestItem: View {
var test:Test
var body:some View{
VStack(spacing: 16.0)
{
Image(test.imageUrl)
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fill)
.frame(width: 300, height: 170)
.cornerRadius(10)
.shadow(radius: 10)
VStack(alignment: .leading, spacing: 5.0)
{
Text(test.imageName)
// .color(.primary)
.font(.headline)
Text(test.description)
.font(.subheadline)
// .color(.secondary)
.multilineTextAlignment(.leading)
.lineLimit(2)
.frame(height: 40)
}
}
}
}
struct OrderButton : View {
var body: some View {
Button(action: {}) {
Text("Order Now")
}.frame(width: 200, height: 50)
.foregroundColor(.white)
.font(.headline)
.background(Color.blue)
.cornerRadius(10)
}
}
class ImageLoader:ObservableObject
{
#Published var data:Data = Data()
func getImage(imageURL:String) {
guard let test = URL(string: imageURL) else { return }
URLSession.shared.dataTask(with: test) { (data, response, error) in
DispatchQueue.main.async {
if let data = data {
self.data = data
}
}
print(data as Any)
}.resume()
}
init(imageURL:String) {
getImage(imageURL: imageURL)
}
}
struct ContentView_Previews: PreviewProvider {
#ObservedObject var imageLoader: ImageLoader
init(test:String)
{
imageLoader = ImageLoader(imageURL: test)
}
static var previews: some View {
ContentView()
}
}
// local.json
[
{
"id":101,
"imageName":"test-f1a",
"imageUrl":"test-f1a",
"description":"test1a",
"category":"test1"
},
...
]
// remote.json
[
{
"id":101,
"imageName":"test-f1a",
"imageUrl":"https://my-url/test-f1a",
"description":"test1a",
"category":"test1"
},
...
]
As of iOS 13 URLSession has been extended with a publisher, so idiomatically your code becomes:
import UIKit
import Combine
struct Test: Codable {
var name: String
}
class NetworkManager: ObservableObject {
#Published var tests:[Test] = [Test]()
private var subscriptions = Set<AnyCancellable>()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://my-url/remote.json")!)
URLSession
.shared
.dataTaskPublisher(for: file)
.map(\.data)
.decode(type: [Test].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.assign(to: \.tests, on: self)
.store(in: &subscriptions)
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
Replacing "grouping: testData" with "grouping: networkManager.tests" and by using "#ObservedObject var networkManager: NetworkManager = NetworkManager()" makes the definition of testData redundant and thus solves the problem. Thanks to #Josh Homann for his answer and comment which helped me to overcome this issue.

Resources