I have a list of URLMedia items, which can be a video or Image from and URL in the internet. I want to show to the user, and for improving the user experience, I want to catch the media locally, so the user can see the next media item immediately without needed to download it.
The interface is ready for testing the functionality. You have the media viewer, and down 2 buttons: 1 for go to next media, one for catch the data. There are 4 items as example, 2 videos 2 images, and you can go in loop over the 4 items over and over.
So far the functionality for the photos works fine, you can perceive the speed difference between going to next media before catching the data and after catching it, is immediate. The problem comes with the video, it does not work, and I can't figure out why. There are some limitations about the tempo folder I am unaware of?
This is the main view
struct ContentView: View {
#StateObject private var AS: AppState = AppState.singleton
func downloadUsingAlamofire(urlInput: URL, index: Int) {
AF.download(urlInput).responseURL { response in
// Read file from provided file URL.
AS.models[index].URLCachedMedia = response.fileURL!.absoluteString
print("Done downloading: \(AS.getCurrentModel().URLCachedMedia)")
print("In task: \(index)")
}
}
var body: some View {
VStack {
switch AS.currentType {
case .image:
AsyncImage(url: URL(string: AS.getCurrentModel().URLCachedMedia))
case .video:
PlayerView(videoURL: AS.getCurrentModel().URLCachedMedia)
}
HStack (spacing: 30) {
Button("Next model") {
AS.nextModel()
print("The URL of the model is: \(AS.getCurrentModel().URLCachedMedia)")
}
Button("Cach media") {
for (index, model) in AS.models.enumerated() {
downloadUsingAlamofire(urlInput: URL(string: model.URLMedia)!, index: index)
}
}
}
}
}
}
Video player view, optimise to auto-play when the video is ready:
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
let player = AVPlayer(url: url)
self.player = player
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
This is the State of the app
class AppState: ObservableObject {
static let singleton = AppState()
private init() {}
#Published var models: [Model] = getModels()
#Published var currentModel: Int = 0
#Published var currentType: TypeMedia = getModels()[0].type
func nextModel() {
if currentModel < models.count - 1 {
currentModel += 1
} else {
currentModel = 0
}
currentType = getCurrentModel().type
}
func getCurrentModel() -> Model {
return models[currentModel]
}
}
And this is the model, with the demo data to test
enum TypeMedia {
case image, video
}
class Model: ObservableObject {
let URLMedia: String
#Published var URLCachedMedia: String
let type: TypeMedia
init(URLMedia: String, type: TypeMedia) {
self.URLMedia = URLMedia
self.type = type
self.URLCachedMedia = self.URLMedia
}
}
func getModels() -> [Model] {
let model1 = Model(URLMedia: "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", type: .video)
let model2 = Model(URLMedia: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4", type: .video)
let model3 = Model(URLMedia: "https://i0.wp.com/www.wikiwiki.in/wp-content/uploads/2021/09/Palki-Sharma-Upadhyay-Journalist-4.jpg?resize=761.25%2C428&ssl=1", type: .image)
let model4 = Model(URLMedia: "https://static.dw.com/image/60451375_403.jpg", type: .image)
return [model1, model3, model2, model4]
}
Related
How can I make a slider when my Data is coming from API? I am using
this(below code) for static images work fine but whenever I try to
use API data then my code does not work.
How to Set the Marquee in this images.
This is My code
public struct MagazineModel: Decodable {
public let magzineBanners: [MagzineBanner]
}
public struct MagzineBanner: Decodable, Identifiable {
public let id: Int
public let url: String
}
This is My View Model
//View Model for Magazines and showing Details
class MagazineBannerVM: ObservableObject{
#Published var datas = [MagzineBanner]()
let url = "ApiUrl"
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(MagazineModel.self, from: data)
DispatchQueue.main.async {
self.datas = results.magzineBanners
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct MagazineBannerView: View{
#ObservedObject var list = MagazineBannerVM()
public let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var currentIndex = 0
#State var totalImages = 2
var body: some View{
ScrollView(.horizontal) {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
HStack{
ForEach(list.datas, id: \.id){ item in
Group{
AsyncImage(url: URL(string: item.url)){ image in
image
.resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}placeholder: {
Image("logo_gray").resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.onReceive(timer, perform: { _ in
withAnimation{
currentIndex = currentIndex < totalImages ? currentIndex + 1: 0
}
})
}
}
}
}
I want to change images after every 2 seconds and every images has
full width as the screen width
And it is showing the half of screen width and showing both images in
single view
I'm learning iOS development and I'm trying to modify my app to use MVVM model. Below I'm pasting json structure that I'm using. I'm able to access categories, but I encountered an issue when I tried to iterate through Items. How my view model should look like? Do I need 2 view models one for Category and another one for Item? Also how to combine View Model with AppStorage?
[
{
"id": "8DC6D7CB-C8E6-4654-BAFE-E89ED7B0AF94",
"name": "Category",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Item1",
"isOn": false
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Item2",
"isOn": false
}
}
]
View
struct ContentView: View {
#ObservedObject var viewModel = MyModel()
var body: some View {
List {
ForEach(viewModel.items, id: \.self) { id in
Text(id.name)
//how to iterate through items?
}
}
}
}
ViewModel
class MyModel: ObservableObject {
#Published var items: [ItemsSection] = [ItemsSection]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "items", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let items = try? JSONDecoder().decode([ItemsSection].self, from: data!)
self.items = items!
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in items {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i + filteredItems.count
}
return i
}
}
Model:
struct ItemSection: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var items: [Item]
}
struct Item: Codable, Equatable, Identifiable,Hashable {
var id: UUID = UUID()
var name: String
var isOn: Bool = false
}
To iterate over the your items array you can do something like this:
struct ContentView: View {
// This should be #StateObject as this View owns the viewmodel
#StateObject var viewModel = MyModel()
var body: some View {
List {
//ItemSection is Identifiable so no need for `id: \.self` here.
ForEach(viewModel.sections) { section in
//Section is a View provided by Apple that can help you laying
// out your View. You don´t have to, you can use your own
Section(section.name){
ForEach(section.items){ item in
Text(item.name)
}
}
}
}
}
}
I´ve changed the naming in thew Viewmodel as I think the naming of items var should really be sections.
class MyModel: ObservableObject {
#Published var sections: [ItemsSection] = [ItemsSection]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "items", withExtension: "json")
else {
print("Json file not found")
return
}
do {
let data = try Data(contentsOf: url)
let sections = try JSONDecoder().decode([ItemsSection].self, from: data)
self.sections = sections
} catch {
print("failed loading or decoding with error: ", error)
}
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in sections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i + filteredItems.count
}
return i
}
}
And never use try? use a proper do / catch block and print the error. This will help you in future to identify problems better. For example the example JSON you provided is malformatted. Without proper do / catch it will just crash while force unwrap.
I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls
I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
I have the following VStack that contains an AVPlayer (in PlayerView):
struct ContentView: View {
#State var url: URL?
private let openFile = NotificationCenter.default.publisher(for: .openFile)
var body: some View {
VStack {
if isVideo(self.url!) {
PlayerView(url: self.url!)
} else {
Image(nsImage: NSImage(contentsOf: self.url!)!).resizable().aspectRatio(contentMode: .fit)
}
}.onReceive(openFile) { notification in
self.url = notification.object as? URL
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And this is the PlayerView:
struct PlayerView: NSViewRepresentable {
private var url: URL
init(url: URL) {
self.url = url
}
func updateNSView(_ nsView: PlayerNSView, context _: NSViewRepresentableContext<PlayerView>) {
nsView.play(url: url)
}
func makeNSView(context _: Context) -> PlayerNSView {
PlayerNSView(frame: .zero)
}
func dismantleNSView(coordinator _: Coordinator) {
// not called
}
}
After updating from a video to a image the audio of the video keeps playing for a few seconds.
Where can I tell the AVPlayer to pause? Does the VStack notify VideoPlayer?
Updated with code from iUrii:
struct ContentView: View {
#State var url: URL?
#State var playerView: PlayerView?
private let openFile = NotificationCenter.default.publisher(for: .openFile)
var body: some View {
VStack {
if let view = playerView {
view
} else {
Image(nsImage: NSImage(contentsOf: self.url!)!).resizable().aspectRatio(contentMode: .fit)
}
}.onReceive(openFile) { notification in
url = notification.object as? URL
if playerView != nil {
playerView!.player.pause()
playerView = nil
}
if videoExtensions.contains(url!.pathExtension.lowercased()) {
playerView = PlayerView(url: url!)
}
}
}
}
This works great when going video to image to video.
When I go video to video, the first video will be paused and audio from the second video starts playing, it is not shown though (the first video is still visible).
I solved it by updating the player with playerView.player.replaceCurrentItem(with: playerItem) instead of replacing the view.
You should manage your PlayerNSView with AVPlayer manually if you want to control its behaviour e.g.:
struct PlayerView: NSViewRepresentable {
let player: AVPlayer
init(url: URL) {
self.player = AVPlayer(url: url)
}
func updateNSView(_ nsView: AVPlayerView, context: NSViewRepresentableContext<Self>) {
}
func makeNSView(context: NSViewRepresentableContext<Self>) -> AVPlayerView {
let playerView = AVPlayerView(frame: .zero)
playerView.player = player
return playerView
}
}
struct ContentView: View {
#State var playerView: PlayerView?
var body: some View {
VStack {
if let view = playerView {
view
} else {
Image(nsImage: NSImage(named: "iphone12")!)
}
Button("Toogle") {
if playerView == nil {
let url = URL(string: "https://www.apple.com/105/media/us/iphone-12-pro/2020/e70ffbd8-50f1-40f3-ac36-0f03a15ac314/films/product/iphone-12-pro-product-tpl-us-2020_16x9.m3u8")!
playerView = PlayerView(url: url)
playerView?.player.play()
}
else {
playerView?.player.pause()
playerView = nil
}
}
}
}
}