Commit 5eb4afef by Dmitriy Stepanets

- Fixed Shorts smooth scrolling issue on Today screen

- Added unlike action
- Fixed shorts binge view UI issue on iOS 12
parent 863fa5cf
...@@ -76,6 +76,7 @@ ...@@ -76,6 +76,7 @@
CD615F7E265523BD00B717DB /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD615F7D265523BD00B717DB /* OneWeatherCore.framework */; }; CD615F7E265523BD00B717DB /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD615F7D265523BD00B717DB /* OneWeatherCore.framework */; };
CD615F7F265523BD00B717DB /* OneWeatherCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD615F7D265523BD00B717DB /* OneWeatherCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CD615F7F265523BD00B717DB /* OneWeatherCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD615F7D265523BD00B717DB /* OneWeatherCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CD615FCC2655299A00B717DB /* NotificationName+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FCB2655299A00B717DB /* NotificationName+Localization.swift */; }; CD615FCC2655299A00B717DB /* NotificationName+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD615FCB2655299A00B717DB /* NotificationName+Localization.swift */; };
CD647A3726B42BA4007C27DA /* ThreadSafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647A3626B42BA4007C27DA /* ThreadSafeDictionary.swift */; };
CD647D0225ED07D60034578B /* TodayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0125ED07D60034578B /* TodayViewModel.swift */; }; CD647D0225ED07D60034578B /* TodayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0125ED07D60034578B /* TodayViewModel.swift */; };
CD647D0625ED08050034578B /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0525ED08050034578B /* ViewModelProtocol.swift */; }; CD647D0625ED08050034578B /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0525ED08050034578B /* ViewModelProtocol.swift */; };
CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD676169262575CD0079D273 /* MapLegendGradientView.swift */; }; CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD676169262575CD0079D273 /* MapLegendGradientView.swift */; };
...@@ -330,6 +331,7 @@ ...@@ -330,6 +331,7 @@
CD593BD22608BC3F00C93428 /* ForecastDayCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDayCell.swift; sourceTree = "<group>"; }; CD593BD22608BC3F00C93428 /* ForecastDayCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDayCell.swift; sourceTree = "<group>"; };
CD615F7D265523BD00B717DB /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD615F7D265523BD00B717DB /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CD615FCB2655299A00B717DB /* NotificationName+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NotificationName+Localization.swift"; sourceTree = "<group>"; }; CD615FCB2655299A00B717DB /* NotificationName+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NotificationName+Localization.swift"; sourceTree = "<group>"; };
CD647A3626B42BA4007C27DA /* ThreadSafeDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeDictionary.swift; sourceTree = "<group>"; };
CD647D0125ED07D60034578B /* TodayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewModel.swift; sourceTree = "<group>"; }; CD647D0125ED07D60034578B /* TodayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewModel.swift; sourceTree = "<group>"; };
CD647D0525ED08050034578B /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = "<group>"; }; CD647D0525ED08050034578B /* ViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelProtocol.swift; sourceTree = "<group>"; };
CD676169262575CD0079D273 /* MapLegendGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegendGradientView.swift; sourceTree = "<group>"; }; CD676169262575CD0079D273 /* MapLegendGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLegendGradientView.swift; sourceTree = "<group>"; };
...@@ -1215,6 +1217,7 @@ ...@@ -1215,6 +1217,7 @@
children = ( children = (
CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */, CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */,
CD37D404260DFFDD002669D6 /* CellFactory.swift */, CD37D404260DFFDD002669D6 /* CellFactory.swift */,
CD647A3626B42BA4007C27DA /* ThreadSafeDictionary.swift */,
); );
path = Common; path = Common;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -1583,6 +1586,7 @@ ...@@ -1583,6 +1586,7 @@
CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */, CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */,
CD593BCC2608A4F200C93428 /* ForecastDailyCell.swift in Sources */, CD593BCC2608A4F200C93428 /* ForecastDailyCell.swift in Sources */,
CD866A72260F6A5300E96A5C /* SettingsDetailsCell.swift in Sources */, CD866A72260F6A5300E96A5C /* SettingsDetailsCell.swift in Sources */,
CD647A3726B42BA4007C27DA /* ThreadSafeDictionary.swift in Sources */,
CD857981267221DD00CC4CDA /* UIDevice+Convenience.swift in Sources */, CD857981267221DD00CC4CDA /* UIDevice+Convenience.swift in Sources */,
CD6761802625B0F50079D273 /* RadarLayerCell.swift in Sources */, CD6761802625B0F50079D273 /* RadarLayerCell.swift in Sources */,
CE13B80F262480B3007CBD4D /* NativeBannerContainerView.swift in Sources */, CE13B80F262480B3007CBD4D /* NativeBannerContainerView.swift in Sources */,
......
//
// ThreadSafeDictionary.swift
// 1Weather
//
// Created by Dmitry Stepanets on 10.07.2021.
//
import Foundation
class ThreadSafeDictionary<V: Hashable,T>: Collection {
private var dictionary: [V: T]
private let concurrentQueue = DispatchQueue(label: "com.oneweather.dictionary",
attributes: .concurrent)
var startIndex: Dictionary<V, T>.Index {
self.concurrentQueue.sync {
return self.dictionary.startIndex
}
}
var endIndex: Dictionary<V, T>.Index {
self.concurrentQueue.sync {
return self.dictionary.endIndex
}
}
init(dict: [V: T] = [V:T]()) {
self.dictionary = dict
}
// this is because it is an apple protocol method
// swiftlint:disable identifier_name
func index(after i: Dictionary<V, T>.Index) -> Dictionary<V, T>.Index {
self.concurrentQueue.sync {
return self.dictionary.index(after: i)
}
}
// swiftlint:enable identifier_name
subscript(key: V) -> T? {
set(newValue) {
self.concurrentQueue.async(flags: .barrier) {[weak self] in
self?.dictionary[key] = newValue
}
}
get {
self.concurrentQueue.sync {
return self.dictionary[key]
}
}
}
// has implicity get
subscript(index: Dictionary<V, T>.Index) -> Dictionary<V, T>.Element {
self.concurrentQueue.sync {
return self.dictionary[index]
}
}
func removeValue(forKey key: V) {
self.concurrentQueue.async(flags: .barrier) {[weak self] in
self?.dictionary.removeValue(forKey: key)
}
}
func removeAll() {
self.concurrentQueue.async(flags: .barrier) {[weak self] in
self?.dictionary.removeAll()
}
}
}
...@@ -68,10 +68,16 @@ class ShortsManager { ...@@ -68,10 +68,16 @@ class ShortsManager {
} }
func like(item: ShortsItem) { func like(item: ShortsItem) {
guard let shortsToLikeIndex = (shorts.firstIndex { $0.id == item.id }) else { guard let shortsToActionIndex = (shorts.firstIndex { $0.id == item.id }) else {
return return
} }
self.shorts[shortsToLikeIndex].like()
if self.shorts[shortsToActionIndex].isLiked {
self.shorts[shortsToActionIndex].dislike()
}
else {
self.shorts[shortsToActionIndex].like()
}
} }
func markAsViewed(item: ShortsItem) { func markAsViewed(item: ShortsItem) {
......
...@@ -164,6 +164,7 @@ class ShortsItemCell: UITableViewCell { ...@@ -164,6 +164,7 @@ class ShortsItemCell: UITableViewCell {
//MARK:- Prepare //MARK:- Prepare
private extension ShortsItemCell { private extension ShortsItemCell {
func prepareForImageOnly() { func prepareForImageOnly() {
clipsToBounds = true
contentView.clipsToBounds = true contentView.clipsToBounds = true
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
......
...@@ -10,7 +10,9 @@ import Nuke ...@@ -10,7 +10,9 @@ import Nuke
import OneWeatherCore import OneWeatherCore
protocol ShortsCollectionCellDelegate: AnyObject { protocol ShortsCollectionCellDelegate: AnyObject {
func averageColor(forImage image:UIImage, identifier:String) -> UIColor? func averageColor(forImage: UIImage,
identifier: String,
completion:@escaping (_ color: UIColor?,_ shortId: String?) -> Void)
} }
class ShortsCollectionViewCell: UICollectionViewCell { class ShortsCollectionViewCell: UICollectionViewCell {
...@@ -40,17 +42,23 @@ class ShortsCollectionViewCell: UICollectionViewCell { ...@@ -40,17 +42,23 @@ class ShortsCollectionViewCell: UICollectionViewCell {
Nuke.loadImage(with: imageRequest, into: imageView) {[weak self] result in Nuke.loadImage(with: imageRequest, into: imageView) {[weak self] result in
switch result { switch result {
case .success(let imageResponse): case .success(let imageResponse):
onMain { self?.delegate?.averageColor(forImage: imageResponse.image,
self?.imageView.image = imageResponse.image identifier: shortsItem.id,
completion:
if let cachedColor = self?.delegate?.averageColor(forImage: imageResponse.image, { avgColor, shortId in
identifier: image.url.absoluteString) { guard shortId == shortsItem.id else {
self?.footerView.backgroundColor = cachedColor return
} }
else {
self?.footerView.backgroundColor = ThemeManager.currentTheme.containerBackgroundColor onMain {
if let color = avgColor {
self?.footerView.backgroundColor = color
}
else {
self?.footerView.backgroundColor = ThemeManager.currentTheme.containerBackgroundColor
}
} }
} })
default: default:
break break
} }
...@@ -68,6 +76,7 @@ class ShortsCollectionViewCell: UICollectionViewCell { ...@@ -68,6 +76,7 @@ class ShortsCollectionViewCell: UICollectionViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
self.imageView.image = nil
self.footerView.backgroundColor = .clear self.footerView.backgroundColor = .clear
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import UIKit import UIKit
import OneWeatherCore import OneWeatherCore
import OneWeatherAnalytics import OneWeatherAnalytics
import Nuke
protocol ShortsViewDelegate: AnyObject { protocol ShortsViewDelegate: AnyObject {
func didSelectShort(at index:Int) func didSelectShort(at index:Int)
...@@ -15,12 +16,15 @@ protocol ShortsViewDelegate: AnyObject { ...@@ -15,12 +16,15 @@ protocol ShortsViewDelegate: AnyObject {
class ShortsView: UIView { class ShortsView: UIView {
//Private //Private
private let prefetcher = ImagePrefetcher()
private let headingLabel = UILabel() private let headingLabel = UILabel()
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: ShortsCollectionViewLayout()) private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: ShortsCollectionViewLayout())
private let avgColorProcessQueue = OperationQueue()
private let avgColorReadQueue = OperationQueue()
private var shorts: [ShortsItem] { private var shorts: [ShortsItem] {
return ShortsManager.shared.shorts return ShortsManager.shared.shorts
} }
private var averageColorCache = [AnyHashable:UIColor]() private var averageColorCache = ThreadSafeDictionary<AnyHashable, UIColor>()
//Public //Public
weak var delegate: ShortsViewDelegate? weak var delegate: ShortsViewDelegate?
...@@ -28,6 +32,9 @@ class ShortsView: UIView { ...@@ -28,6 +32,9 @@ class ShortsView: UIView {
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
//Setup queues
avgColorReadQueue.maxConcurrentOperationCount = 1
prepareCollectionView() prepareCollectionView()
} }
...@@ -45,12 +52,14 @@ class ShortsView: UIView { ...@@ -45,12 +52,14 @@ class ShortsView: UIView {
//MARK:- Prepare //MARK:- Prepare
private extension ShortsView { private extension ShortsView {
func prepareCollectionView() { func prepareCollectionView() {
collectionView.isPrefetchingEnabled = true
collectionView.showsVerticalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor collectionView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
collectionView.register(ShortsCollectionViewCell.self, forCellWithReuseIdentifier: ShortsCollectionViewCell.kIdentifier) collectionView.register(ShortsCollectionViewCell.self, forCellWithReuseIdentifier: ShortsCollectionViewCell.kIdentifier)
collectionView.dataSource = self collectionView.dataSource = self
collectionView.delegate = self collectionView.delegate = self
// collectionView.prefetchDataSource = self
addSubview(collectionView) addSubview(collectionView)
collectionView.snp.makeConstraints { make in collectionView.snp.makeConstraints { make in
...@@ -60,7 +69,7 @@ private extension ShortsView { ...@@ -60,7 +69,7 @@ private extension ShortsView {
} }
} }
//MARK: UICollectionView Data Source //MARK:- UICollectionView Data Source
extension ShortsView: UICollectionViewDataSource { extension ShortsView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return shorts.count return shorts.count
...@@ -75,6 +84,7 @@ extension ShortsView: UICollectionViewDataSource { ...@@ -75,6 +84,7 @@ extension ShortsView: UICollectionViewDataSource {
} }
} }
//MARK:- UICollectionView Delegate
extension ShortsView: UICollectionViewDelegate { extension ShortsView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == 0 { if indexPath.row == 0 {
...@@ -105,19 +115,56 @@ extension ShortsView: UICollectionViewDelegate { ...@@ -105,19 +115,56 @@ extension ShortsView: UICollectionViewDelegate {
} }
} }
//MARK:- UICollectionView Data Source Prefetching
extension ShortsView: UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
let urls = shorts.map{ $0.images[0].url }
prefetcher.startPrefetching(with: urls)
}
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
let urls = shorts.map{ $0.images[0].url }
prefetcher.stopPrefetching(with: urls)
}
}
//MARK:- ShortsCollectionCell Delegate //MARK:- ShortsCollectionCell Delegate
extension ShortsView: ShortsCollectionCellDelegate { extension ShortsView: ShortsCollectionCellDelegate {
func averageColor(forImage image: UIImage, identifier: String) -> UIColor? { func averageColor(forImage: UIImage,
identifier: String,
completion: @escaping (UIColor?, String?) -> Void
) {
// self.avgColorReadQueue.addOperation {
// if let cachedColor = self.averageColorCache[identifier] {
// completion(cachedColor, identifier)
// }
// else {
// self.avgColorProcessQueue.addOperation {
// if let color = forImage.averageColor {
// self.avgColorReadQueue.addOperation {
// self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30)
// }
// completion(color, identifier)
// }
// else {
// completion(nil, identifier)
// }
// }
// }
// }
if let cachedColor = self.averageColorCache[identifier] { if let cachedColor = self.averageColorCache[identifier] {
return cachedColor completion(cachedColor, identifier)
} }
else { else {
if let color = image.averageColor { self.avgColorProcessQueue.addOperation {
self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30) if let color = forImage.averageColor {
return color self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30)
} completion(color, identifier)
else { }
return nil else {
completion(nil, identifier)
}
} }
} }
} }
......
...@@ -92,6 +92,10 @@ public struct ShortsItem { ...@@ -92,6 +92,10 @@ public struct ShortsItem {
isLiked = true isLiked = true
} }
public mutating func dislike() {
isLiked = false
}
public mutating func markAsViewed() { public mutating func markAsViewed() {
isViewed = true isViewed = true
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment