i'm currently struggling to fetch any changes from an published variable in SwiftUI. Most of the code is created after this tutorial on YouTube.
It's basically an app, that fetches cryptos from a firebase database. To avoid high server costs I want to update any changes of the coins to the database but not have an observer to lower the download rate.
What's the bug?
When I'm adding a coin to my favorites, it sends the data correctly to the database and updates the UI. However when I try to filter the coins the Coin-array switches back to it's previous state. I also added a breakpoint on the CoinCellViewModel(coin: coin)-Line but it only gets executed when I change the filterBy. Here's a little visualisation of the bug:
Repository
class CoinsRepository: ObservableObject {
#Published var coins = [Coin]()
var ref: DatabaseReference!
init() {
self.ref = Database.database().reference()
loadDatabase(ref)
}
func loadDatabase(_ ref: DatabaseReference) {
ref.child("coins").observeSingleEvent(of: .value) { snapshot in
guard let dictionaries = snapshot.value as? [String: Any] else { return }
var coinNames: [String] = []
self.coins = dictionaries.compactMap({ (key: String, value: Any) in
guard let dic = value as? [String: Any] else { return nil }
coinNames.append(dic["name"] as? String ?? "")
return Coin(dic)
})
}
}
func updateFavorite(_ coin: Coin, state: Bool) {
let path = ref.child("coins/\(coin.name)")
var flag = false
path.updateChildValues(["favorite": state]) { err, ref in
if let err = err {
print("ERROR: \(err.localizedDescription)")
} else {
var i = 0
var newCoinArray = self.coins
for coinA in newCoinArray {
if coinA.name == coin.name {
newCoinArray[i].favorite = state
}
i += 1
}
// I guess here's the error
DispatchQueue.main.async {
self.objectWillChange.send()
self.coins = newCoinArray
}
}
}
}
}
ViewModel
class CoinListViewModel: ObservableObject {
#Published var coinRepository = CoinsRepository()
#Published var coinCellViewModels = [CoinCellViewModel]()
#Published var filterBy: [Bool] = UserDefaults.standard.array(forKey: "filter") as? [Bool] ?? [false, false, false]
#Published var fbPrice: Double = 0.00
#Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.combineLatest(coinRepository.$coins, $fbPrice, $filterBy)
.map(filter)
.sink { coins in
self.coinCellViewModels = coins.map { coin in
CoinCellViewModel(coin: coin)
}
}
.store(in: &cancellables)
}
...
}
updateFavorite(_ coin: Coin, state: Bool) get's called in the CoinCellViewModel() but I guess the code isn't necessary here...
I'm fairly new to the Combine topic and not quite getting all the new methods, so any help is appreciated!
Related
I have a form where the user enters their address. While they can always enter it manually, I also wanted to provide them with an easy solution with auto complete so that they could just start typing their address and then tap on the correct one from the list and have it auto populate the various fields.
I started by working off of jnpdx's Swift5 solution - https://stackoverflow.com/a/67131376/11053343
However, there are two issues that I cannot seem to solve:
I need the results to be limited to the United States only (not just the continental US, but the entire United States including Alaska, Hawaii, and Puerto Rico). I am aware of how MKCoordinateRegion works with the center point and then the zoom spread, but it doesn't seem to work on the results of the address search.
The return of the results provides only a title and subtitle, where I need to actually extract all the individual address information and populate my variables (i.e. address, city, state, zip, and zip ext). If the user has an apt or suite number, they would then fill that in themselves. My thought was to create a function that would run when the button is tapped, so that the variables are assigned based off of the user's selection, but I have no idea how to extract the various information required. Apple's docs are terrible as usual and I haven't found any tutorials explaining how to do this.
This is for the latest SwiftUI and XCode (ios15+).
I created a dummy form for testing. Here's what I have:
import SwiftUI
import Combine
import MapKit
class MapSearch : NSObject, ObservableObject {
#Published var locationResults : [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables : Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.region = MKCoordinateRegion()
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm)
})
.sink(receiveCompletion: { (completion) in
//handle error
}, receiveValue: { (results) in
self.locationResults = results
})
.store(in: &cancellables)
}
func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch : MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//currentPromise?(.failure(error))
}
}
struct MapKit_Interface: View {
#StateObject private var mapSearch = MapSearch()
#State private var address = ""
#State private var addrNum = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
#State private var zipExt = ""
var body: some View {
List {
Section {
TextField("Search", text: $mapSearch.searchTerm)
ForEach(mapSearch.locationResults, id: \.self) { location in
Button {
// Function code goes here
} label: {
VStack(alignment: .leading) {
Text(location.title)
.foregroundColor(Color.white)
Text(location.subtitle)
.font(.system(.caption))
.foregroundColor(Color.white)
}
} // End Label
} // End ForEach
} // End Section
Section {
TextField("Address", text: $address)
TextField("Apt/Suite", text: $addrNum)
TextField("City", text: $city)
TextField("State", text: $state)
TextField("Zip", text: $zip)
TextField("Zip-Ext", text: $zipExt)
} // End Section
} // End List
} // End var Body
} // End Struct
Since no one has responded, I, and my friend Tolstoy, spent a lot of time figuring out the solution and I thought I would post it for anyone else who might be interested. Tolstoy wrote a version for the Mac, while I wrote the iOS version shown here.
Seeing as how Google is charging for usage of their API and Apple is not, this solution gives you address auto-complete for forms. Bear in mind it won't always be perfect because we are beholden to Apple and their maps. Likewise, you have to turn the address into coordinates, which you then turn into a placemark, which means there will be some addresses that may change when tapped from the completion list. Odds are this won't be an issue for 99.9% of users, but thought I would mention it.
At the time of this writing, I am using XCode 13.2.1 and SwiftUI for iOS 15.
I organized it with two Swift files. One to hold the class/struct (AddrStruct.swift) and the other which is the actual view in the app.
AddrStruct.swift
import SwiftUI
import Combine
import MapKit
import CoreLocation
class MapSearch : NSObject, ObservableObject {
#Published var locationResults : [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables : Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm)
})
.sink(receiveCompletion: { (completion) in
//handle error
}, receiveValue: { (results) in
self.locationResults = results.filter { $0.subtitle.contains("United States") } // This parses the subtitle to show only results that have United States as the country. You could change this text to be Germany or Brazil and only show results from those countries.
})
.store(in: &cancellables)
}
func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch : MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//could deal with the error here, but beware that it will finish the Combine publisher stream
//currentPromise?(.failure(error))
}
}
struct ReversedGeoLocation {
let streetNumber: String // eg. 1
let streetName: String // eg. Infinite Loop
let city: String // eg. Cupertino
let state: String // eg. CA
let zipCode: String // eg. 95014
let country: String // eg. United States
let isoCountryCode: String // eg. US
var formattedAddress: String {
return """
\(streetNumber) \(streetName),
\(city), \(state) \(zipCode)
\(country)
"""
}
// Handle optionals as needed
init(with placemark: CLPlacemark) {
self.streetName = placemark.thoroughfare ?? ""
self.streetNumber = placemark.subThoroughfare ?? ""
self.city = placemark.locality ?? ""
self.state = placemark.administrativeArea ?? ""
self.zipCode = placemark.postalCode ?? ""
self.country = placemark.country ?? ""
self.isoCountryCode = placemark.isoCountryCode ?? ""
}
}
For testing purposes, I called my main view file Test.swift. Here's a stripped down version for reference.
Test.swift
import SwiftUI
import Combine
import CoreLocation
import MapKit
struct Test: View {
#StateObject private var mapSearch = MapSearch()
func reverseGeo(location: MKLocalSearchCompletion) {
let searchRequest = MKLocalSearch.Request(completion: location)
let search = MKLocalSearch(request: searchRequest)
var coordinateK : CLLocationCoordinate2D?
search.start { (response, error) in
if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate {
coordinateK = coordinate
}
if let c = coordinateK {
let location = CLLocation(latitude: c.latitude, longitude: c.longitude)
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard let placemark = placemarks?.first else {
let errorString = error?.localizedDescription ?? "Unexpected Error"
print("Unable to reverse geocode the given location. Error: \(errorString)")
return
}
let reversedGeoLocation = ReversedGeoLocation(with: placemark)
address = "\(reversedGeoLocation.streetNumber) \(reversedGeoLocation.streetName)"
city = "\(reversedGeoLocation.city)"
state = "\(reversedGeoLocation.state)"
zip = "\(reversedGeoLocation.zipCode)"
mapSearch.searchTerm = address
isFocused = false
}
}
}
}
// Form Variables
#FocusState private var isFocused: Bool
#State private var btnHover = false
#State private var isBtnActive = false
#State private var address = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
// Main UI
var body: some View {
VStack {
List {
Section {
Text("Start typing your street address and you will see a list of possible matches.")
} // End Section
Section {
TextField("Address", text: $mapSearch.searchTerm)
// Show auto-complete results
if address != mapSearch.searchTerm && isFocused == false {
ForEach(mapSearch.locationResults, id: \.self) { location in
Button {
reverseGeo(location: location)
} label: {
VStack(alignment: .leading) {
Text(location.title)
.foregroundColor(Color.white)
Text(location.subtitle)
.font(.system(.caption))
.foregroundColor(Color.white)
}
} // End Label
} // End ForEach
} // End if
// End show auto-complete results
TextField("City", text: $city)
TextField("State", text: $state)
TextField("Zip", text: $zip)
} // End Section
.listRowSeparator(.visible)
} // End List
} // End Main VStack
} // End Var Body
} // End Struct
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
If anyone is wondering how to generate global results, change the code from this:
self.locationResults = results.filter{$0.subtitle.contains("United States")}
to this in Address Structure file:
self.locationResults = results
I want to develop view that's loading data from Health kit (mindfulness time) so I used Timer every 1 minute to get a new data from Health kit, created by Apple watch but onReceive(Timer) are not refreshing a new data (it pass previous data only)
if I Open another app and come back to this app then it's show me a new data
import SwiftUI
struct LoadingView: View {
var healthStore : HealthStore? = HealthStore()
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var num : Int = 0
#Binding var showModal: Bool
var decription : String
// MARK: - BODY
var body: some View {
VStack (alignment: .center){
Spacer()
WatchView()
Spacer()
Text(decription)
.font(.callout)
.fontWeight(.semibold)
.padding(.horizontal,30)
Spacer()
}
.navigationBarBackButtonHidden(true)
.onReceive(timer) { _ in
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.getDailyMindfulnessTime { time in
print("\(time)")
}
} //: SUCCESS
}
}
}// ON RECEIVE
.onDisappear(perform: {
self.timer.upstream.connect().cancel()
})//ON DISAPPEAR
}
}
Health Store
import Foundation
import HealthKit
class HealthStore {
var healthStore : HKHealthStore?
var query : HKStatisticsCollectionQuery?
var querySampleQuery : HKSampleQuery?
init(){
// to check data is avaliable or not?
if HKHealthStore.isHealthDataAvailable(){
//Create instance of HKHealthStore
healthStore = HKHealthStore()
}
}
// Authorization
func requestAuthorization(compleion: #escaping(Bool)-> Void){
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let mindfulSampleType = HKSampleType.categoryType(forIdentifier: .mindfulSession)!
guard let healthStore = self.healthStore else { return compleion(false)}
healthStore.requestAuthorization(toShare: [], read: [stepType,mindfulSampleType]) { (success, error) in
compleion(success)
}
}
//Calculate steps count
func calculateSteps(completion : #escaping(HKStatisticsCollection?)->Void){
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let startDate = Calendar.current.date(byAdding: .day,value: -7, to: Date())
let anchorDate = Date.mondayAt12AM()
let daily = DateComponents(day:1)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date()
, options: .strictStartDate)
//cumulativeSum (Watch+Iphone)
query = HKStatisticsCollectionQuery(quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: daily)
query!.initialResultsHandler = { query, statisticsCollection , error in
completion(statisticsCollection)
}
if let healthStore = self.healthStore, let query = self.query {
healthStore.execute(query)
}
}
// DailyMindfulnessTime
func getDailyMindfulnessTime(completion: #escaping (TimeInterval) -> Void) {
let sampleType = HKSampleType.categoryType(forIdentifier: .mindfulSession)!
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let startDate = Calendar.current.startOfDay(for: Date())
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
querySampleQuery = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { (_, results, error) in
if error != nil {
print(" HealthKit returned error while trying to query today's mindful sessions. The error was: \(String(describing: error?.localizedDescription))")
}
if let results = results {
var totalTime = TimeInterval()
for result in results {
totalTime += result.endDate.timeIntervalSince(result.startDate)
}
completion(totalTime)
} else {
completion(0)
}
}
if let healthStore = self.healthStore, let querySampleQuery = self.querySampleQuery {
healthStore.execute(querySampleQuery)
}
}
}
extension Date {
static func mondayAt12AM() -> Date{
return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear,.weekOfYear],from: Date()))!
}
}
first of all you write in your question that you want to update every minute, but currently you update every second. TimeInterval is a typealias for Double and you pass it in your Timer as seconds. So in your case it should be:
#State private var timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
Be aware this means onReceive is called after 60 seconds and not immediately.
And I tested your code and it worked fine for me. Unfortunately you didnt include your watch view so I dont know what you are doing in there.
I assumed num is the variable you wanted to update, so you need to call:
num += Int(time)
in your closure for getDailyMindfulnessTime.
If you want to display the time in your WatchView make sure to pass num as a Binding in there.
I'm storing ~100.000 dictionary entries in a realm database and would like to display them. Additionally I want to filter them by a search field. Now I'm running in a problem: The search function is really inefficient although I've tried to debounce the search.
View Model:
class DictionaryViewModel : ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var entries: Results<DictionaryEntry>?
#Published var filteredEntries: Results<DictionaryEntry>?
#Published var searchText: String = ""
#Published var isSearching: Bool = false
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(800), scheduler: RunLoop.main) // debounces the string publisher, such that it delays the process of sending request to remote server.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.filteredEntries = nil
return nil
}
return string
})
.compactMap{ $0 }
.sink { (_) in
} receiveValue: { [self] (searchField) in
filter(with: searchField)
}.store(in: &subscription)
self.fetch()
}
public func fetch(){
self.entries = DatabaseManager.sharedInstance.fetchData(type: DictionaryEntry.self).sorted(byKeyPath: "pinyin", ascending: true)
self.filteredEntries = entries
}
public func filter(with condition: String){
self.filteredEntries = self.entries?.filter("pinyin CONTAINS[cd] %#", searchText).sorted(byKeyPath: "pinyin", ascending: true)
}
In my View I'm just displaying the filteredEtries in a ScrollView
The debouncing works well for short text inputs like "hello", but when I filter for "this is a very long string" my UI freezes. I'm not sure whether something with my debounce function is wrong or the way I handle the data filtering in very inefficient.
EDIT: I've noticed that the UI freezes especially when the result is empty.
EDIT 2:
The .fetchData() function is just this here:
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
All realm objects have a primary key. The structure looks like this:
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var character: String = ""
#objc dynamic var pinyin: String = ""
#objc dynamic var translation: String = ""
override class func primaryKey() -> String {
return "id"
}
EDIT 3: The filtered results are displayed this way:
ScrollView{
LazyVGrid(columns: gridItems, spacing: 0){
if (dictionaryViewModel.filteredEntries != nil) {
ForEach(dictionaryViewModel.filteredEntries!){ entry in
Text("\(entry.translation)")
}
} else {
Text("No results found")
}
}
I want to add a Button to my view to load more data. In my Environment Object the data is generated randomly via an API.
How can I reload my Environment object to get new items. The Code Below should make it clear. Thanks in advance
class observer : ObservableObject{
#Published var shows = [stacks]()
#Published var last = -1
var results = [Result1]()
init(){
let number = Int.random(in: 1...35)
print("das ist dier etste radodm nube: \(number)")
let endnumber = number + 8
print("das ist dier etste radodm nube: \(endnumber)")
for n in number...endnumber{
guard let url = URL(string:"https://api.themoviedb.org/3/discover/tv?api_key=3ed2bd15f916d0e3fbb77c193bf33b61&language=de-DE®ion=DE&with_networks=213&page=\(n)" ) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
for i in self.results{
self.shows.append(stacks(id: "\(i.id)", name: i.name, typ: "Serie", status: "",overview: i.overview, vote: "\(i.vote_average)", image: i.poster_path, swipe: 0, degree: 0, commercal: "no"))
}
self.shows.shuffle()
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
}
struct View: View {
#EnvironmentObject var cards : observer
var body: some View {
VStack{
Button(action: {
//reload
}){
Text("reload")
}
}
I would put the fetching/loading data code that is inside the init in a function.
func codeToFetchData() {
//Code from your init
}
The call that method in your init and your Button for example cards.codeToFetchData()
I am new to Swiftui and I struggle to understand how to properly retain data created in ObservableObject when rendering views? Or a completely different approach to the problem maybe?
More specifically, it is about getting HTTP data in each row in a List().
Right now, it makes the HTTP call far too often when parent views are rendered, which causes all rows to be reloaded.
The same issue can be found here: Keep reference on view/data model after View update
public class VideoFetcher: ObservableObject {
#Published var video: VideoResponse?
#Published var coverImage: UIImage?
#Published var coverImageLoading = false
#Published var categories: String?
#Published var loading = false
#Published var error = false
func load(mediaItemSlug: String = "", broadcasterSlug: String = "") {
self.loading = true
Video.findBySlug(
mediaItemSlug: mediaItemSlug,
broadcasterSlug: broadcasterSlug,
successCallback: {video -> Void in
self.video = video
self.loading = false
self.setCategories()
self.loadCoverImage()
},
errorCallback: {(error, _) -> Void in
self.loading = false
self.error = true
})
}
func loadCoverImage() {
guard self.video!.coverImageUrl != "" else {
return
}
self.coverImageLoading = true
let downloader = ImageDownloader()
let urlRequest = URLRequest(url: URL(string: self.video!.coverImageUrl)!)
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 520.0, height: 292.499999963))
downloader.download(urlRequest, filter: filter) { response in
if case .success(let image) = response.result {
self.coverImage = image
self.coverImageLoading = false
}
}
}
func setCategories() {
if (self.video!.broadcaster.categories.count > 0) {
let categoryNames = self.video!.broadcaster.categories.map { category in
return category.name == "" ? "(no name)" : category.name
}
self.categories = categoryNames.joined(separator: " • ");
}
}
}
List() row:
struct VideoCard: View {
#ObservedObject var fetcher = VideoFetcher()
...
init() {
// Causes reload each render
self.fetcher.load()
}
var body: some View {
...
.onAppear {
// Loads that on appear but fetcher.video is nil after view re-rendered because load() wasn't called
self.fetcher.load()
}
}
}
Thanks, Chris. I thought I was doing something wrong on an architectural level but I added caching and that solved my problem.
import Alamofire
import AlamofireImage
import Cache
public class VideoFetcher: ObservableObject {
#Published var video: VideoResponse?
#Published var coverImage: UIImage?
#Published var coverImageLoading = false
#Published var broadcasterImage: UIImage?
#Published var categories: String?
#Published var loading = false
#Published var error = false
func load(mediaItemSlug: String = "", broadcasterSlug: String = "") {
let videoCache = try? AppCache.video!.object(forKey: mediaItemSlug)
if (videoCache != nil) {
self.video = videoCache
self.setCategories()
self.loadCoverImage()
return
}
self.loading = true
Video.findBySlug(
mediaItemSlug: mediaItemSlug,
broadcasterSlug: broadcasterSlug,
successCallback: {video -> Void in
try? AppCache.video!.setObject(video, forKey: mediaItemSlug)
self.video = video
self.loading = false
self.setCategories()
self.loadCoverImage()
self.loadBroadcasterImage()
},
errorCallback: {(error, _) -> Void in
self.loading = false
self.error = true
})
}
func loadCoverImage() {
let coverImageUrl = self.video!.coverImageUrl
guard coverImageUrl != "" else {
return
}
let urlRequest = URLRequest(url: URL(string: coverImageUrl)!)
let cachedImage = AppCache.image!.image(for: urlRequest, withIdentifier: coverImageUrl)
if (cachedImage != nil) {
self.coverImage = cachedImage
return
}
self.coverImageLoading = true
let downloader = ImageDownloader(imageCache: AppCache.image!)
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 520.0, height: 292.499999963))
downloader.download(urlRequest, filter: filter) { response in
if case .success(let image) = response.result {
AppCache.image!.add(image, for: urlRequest, withIdentifier: coverImageUrl)
self.coverImage = image
self.coverImageLoading = false
}
}
}
func loadBroadcasterImage() {
let broadcasterImage = self.video!.broadcaster.avatarImageUrl
guard broadcasterImage != "" else {
return
}
let urlRequest = URLRequest(url: URL(string: broadcasterImage)!)
let cachedImage = AppCache.image!.image(for: urlRequest, withIdentifier: broadcasterImage)
if (cachedImage != nil) {
self.broadcasterImage = cachedImage
return
}
let downloader = ImageDownloader(imageCache: AppCache.image!)
let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 16, height: 16))
downloader.download(urlRequest, filter: filter) { response in
if case .success(var image) = response.result {
image = image.af.imageRoundedIntoCircle()
AppCache.image!.add(image, for: urlRequest, withIdentifier: broadcasterImage)
self.broadcasterImage = image
}
}
}
func setCategories() {
let categories = self.video!.broadcaster.categories
if (categories.count > 0) {
let categoryNames = categories.map { category in
return category.name == "" ? "(no name)" : category.name
}
self.categories = categoryNames.joined(separator: " • ");
}
}
}