diff --git a/vrtnu/vrtnu.xcodeproj/project.pbxproj b/vrtnu/vrtnu.xcodeproj/project.pbxproj index 6ab3347..8d2335a 100644 --- a/vrtnu/vrtnu.xcodeproj/project.pbxproj +++ b/vrtnu/vrtnu.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + B408F0D7251F6B760043E3A4 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = B408F0D6251F6B760043E3A4 /* Tools.swift */; }; + B408F0DD251F6D180043E3A4 /* AsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B408F0DC251F6D180043E3A4 /* AsyncImage.swift */; }; B4F0CC7B251BE62B00E9EA74 /* vrtnuApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F0CC7A251BE62B00E9EA74 /* vrtnuApp.swift */; }; B4F0CC7D251BE62B00E9EA74 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F0CC7C251BE62B00E9EA74 /* ContentView.swift */; }; B4F0CC7F251BE62F00E9EA74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4F0CC7E251BE62F00E9EA74 /* Assets.xcassets */; }; @@ -35,6 +37,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B408F0D6251F6B760043E3A4 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; + B408F0DC251F6D180043E3A4 /* AsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImage.swift; sourceTree = ""; }; B4F0CC77251BE62B00E9EA74 /* vrtnu.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = vrtnu.app; sourceTree = BUILT_PRODUCTS_DIR; }; B4F0CC7A251BE62B00E9EA74 /* vrtnuApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = vrtnuApp.swift; sourceTree = ""; }; B4F0CC7C251BE62B00E9EA74 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -76,6 +80,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B408F0DB251F6D030043E3A4 /* AscyncImage */ = { + isa = PBXGroup; + children = ( + B408F0DC251F6D180043E3A4 /* AsyncImage.swift */, + ); + path = AscyncImage; + sourceTree = ""; + }; B4F0CC6E251BE62B00E9EA74 = { isa = PBXGroup; children = ( @@ -99,6 +111,7 @@ B4F0CC79251BE62B00E9EA74 /* vrtnu */ = { isa = PBXGroup; children = ( + B408F0DB251F6D030043E3A4 /* AscyncImage */, B4F0CC7A251BE62B00E9EA74 /* vrtnuApp.swift */, B4F0CC7C251BE62B00E9EA74 /* ContentView.swift */, B4F0CC7E251BE62F00E9EA74 /* Assets.xcassets */, @@ -106,6 +119,7 @@ B4F0CC80251BE62F00E9EA74 /* Preview Content */, B4F0CCA9251BFD6F00E9EA74 /* video.swift */, B4F0CCB6251D6B5400E9EA74 /* VrtNuLayout.swift */, + B408F0D6251F6B760043E3A4 /* Tools.swift */, ); path = vrtnu; sourceTree = ""; @@ -269,6 +283,8 @@ B4F0CCB7251D6B5400E9EA74 /* VrtNuLayout.swift in Sources */, B4F0CC7B251BE62B00E9EA74 /* vrtnuApp.swift in Sources */, B4F0CCAA251BFD6F00E9EA74 /* video.swift in Sources */, + B408F0DD251F6D180043E3A4 /* AsyncImage.swift in Sources */, + B408F0D7251F6B760043E3A4 /* Tools.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/vrtnu/vrtnu/AscyncImage/AsyncImage.swift b/vrtnu/vrtnu/AscyncImage/AsyncImage.swift new file mode 100644 index 0000000..d8271bc --- /dev/null +++ b/vrtnu/vrtnu/AscyncImage/AsyncImage.swift @@ -0,0 +1,122 @@ +// +// AsyncImage.swift +// AsyncImage +// +// Created by Vadym Bulavin on 2/13/20. +// Copyright © 2020 Vadym Bulavin. All rights reserved. +// +import SwiftUI +import UIKit +import Combine + +protocol ImageCache { + subscript(_ url: URL) -> UIImage? { get set } +} + +struct TemporaryImageCache: ImageCache { + private let cache = NSCache() + + subscript(_ key: URL) -> UIImage? { + get { cache.object(forKey: key as NSURL) } + set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) } + } +} + +struct ImageCacheKey: EnvironmentKey { + static let defaultValue: ImageCache = TemporaryImageCache() +} + +extension EnvironmentValues { + var imageCache: ImageCache { + get { self[ImageCacheKey.self] } + set { self[ImageCacheKey.self] = newValue } + } +} + +struct AsyncImage: View { + @StateObject private var loader: ImageLoader + private let placeholder: Placeholder + private let image: (UIImage) -> Image + + init( + url: URL, + @ViewBuilder placeholder: () -> Placeholder, + @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:) + ) { + self.placeholder = placeholder() + self.image = image + _loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue)) + } + + var body: some View { + content + .onAppear(perform: loader.load) + } + + private var content: some View { + Group { + if loader.image != nil { + image(loader.image!) + } else { + placeholder + } + } + } +} + +class ImageLoader: ObservableObject { + @Published var image: UIImage? + + private(set) var isLoading = false + + private let url: URL + private var cache: ImageCache? + private var cancellable: AnyCancellable? + + private static let imageProcessingQueue = DispatchQueue(label: "image-processing") + + init(url: URL, cache: ImageCache? = nil) { + self.url = url + self.cache = cache + } + + deinit { + cancel() + } + + func load() { + guard !isLoading else { return } + + if let image = cache?[url] { + self.image = image + return + } + + cancellable = URLSession.shared.dataTaskPublisher(for: url) + .map { UIImage(data: $0.data) } + .replaceError(with: nil) + .handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() }, + receiveOutput: { [weak self] in self?.cache($0) }, + receiveCompletion: { [weak self] _ in self?.onFinish() }, + receiveCancel: { [weak self] in self?.onFinish() }) + .subscribe(on: Self.imageProcessingQueue) + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.image = $0 } + } + + func cancel() { + cancellable?.cancel() + } + + private func onStart() { + isLoading = true + } + + private func onFinish() { + isLoading = false + } + + private func cache(_ image: UIImage?) { + image.map { cache?[url] = $0 } + } +} diff --git a/vrtnu/vrtnu/ContentView.swift b/vrtnu/vrtnu/ContentView.swift index 980d89e..3a4df32 100644 --- a/vrtnu/vrtnu/ContentView.swift +++ b/vrtnu/vrtnu/ContentView.swift @@ -8,20 +8,29 @@ import SwiftUI import AVKit import AVFoundation +import Combine + + struct ContentView: View { var body: some View { NavigationView{ - List { + VStack { ForEach(VRTNu().shows, id: \.self){ show in - Section(header: Text(show.title)){ + VStack{ + Text(show.title) HStack{ ForEach(show.seasons, id:\.self){ season in - HStack{ - ForEach(season.episodes, id:\.self){ episode in + ForEach(season.episodes, id:\.self){ episode in + NavigationLink(destination: VideoView(url: episode.video.hlsUrl)){VStack{ Text(season.seasonName) Text(episode.name) - } + AsyncImage( + url: episode.imageurl, + placeholder: { Text("Loading ...") }, + image: { Image(uiImage: $0).resizable() } + ) + }} } } } @@ -34,16 +43,19 @@ struct ContentView: View { } } + /*struct SeasonView: View{ var body: some View{ } + URL(string: "https://remix-cf.lwc.vrtcdn.be/remix/ecd69313-4a39-4297-95b1-aede167725b7/remix.ism/.m3u8")! }*/ struct VideoView: View { - private let player = AVPlayer(url: URL(string: "https://remix-cf.lwc.vrtcdn.be/remix/ecd69313-4a39-4297-95b1-aede167725b7/remix.ism/.m3u8")!); + var url: URL var body: some View { + let player = AVPlayer(url: url); VideoPlayer(player: player).fixedSize() }; diff --git a/vrtnu/vrtnu/Tools.swift b/vrtnu/vrtnu/Tools.swift new file mode 100644 index 0000000..be13b24 --- /dev/null +++ b/vrtnu/vrtnu/Tools.swift @@ -0,0 +1,9 @@ +// +// Tools.swift +// vrtnu +// +// Created by Jens Timmerman on 26/09/2020. +// + +import Foundation + diff --git a/vrtnu/vrtnu/VrtNuLayout.swift b/vrtnu/vrtnu/VrtNuLayout.swift index 778b9e3..0a065c7 100644 --- a/vrtnu/vrtnu/VrtNuLayout.swift +++ b/vrtnu/vrtnu/VrtNuLayout.swift @@ -7,6 +7,7 @@ import Foundation + struct Episode: Hashable{ let name: String let video: Video @@ -76,6 +77,9 @@ struct VRTNu { init() { //TODO: fetch shows - self.shows = [Show(showUrl: URL(string: "https://www.vrt.be/vrtnu/a-z/het-peulengaleis/")!, title: "Het Peulengaleis")] + self.shows = [ + Show(showUrl: URL(string: "https://www.vrt.be/vrtnu/a-z/het-peulengaleis/")!, title: "Het Peulengaleis"), + Show(showUrl: URL(string: "https://www.vrt.be/vrtnu/a-z/het-peulengaleis2/")!, title: "Het Peulengaleis 2"), + ] } }