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 @@
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, ); }; };
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 */; };
CD647D0625ED08050034578B /* ViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD647D0525ED08050034578B /* ViewModelProtocol.swift */; };
CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD676169262575CD0079D273 /* MapLegendGradientView.swift */; };
......@@ -330,6 +331,7 @@
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; };
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>"; };
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>"; };
......@@ -1215,6 +1217,7 @@
children = (
CDA5542C25EF7C9700A2E08C /* ReusableCellProtocol.swift */,
CD37D404260DFFDD002669D6 /* CellFactory.swift */,
CD647A3626B42BA4007C27DA /* ThreadSafeDictionary.swift */,
);
path = Common;
sourceTree = "<group>";
......@@ -1583,6 +1586,7 @@
CD67616A262575CD0079D273 /* MapLegendGradientView.swift in Sources */,
CD593BCC2608A4F200C93428 /* ForecastDailyCell.swift in Sources */,
CD866A72260F6A5300E96A5C /* SettingsDetailsCell.swift in Sources */,
CD647A3726B42BA4007C27DA /* ThreadSafeDictionary.swift in Sources */,
CD857981267221DD00CC4CDA /* UIDevice+Convenience.swift in Sources */,
CD6761802625B0F50079D273 /* RadarLayerCell.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 {
}
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
}
self.shorts[shortsToLikeIndex].like()
if self.shorts[shortsToActionIndex].isLiked {
self.shorts[shortsToActionIndex].dislike()
}
else {
self.shorts[shortsToActionIndex].like()
}
}
func markAsViewed(item: ShortsItem) {
......
......@@ -164,6 +164,7 @@ class ShortsItemCell: UITableViewCell {
//MARK:- Prepare
private extension ShortsItemCell {
func prepareForImageOnly() {
clipsToBounds = true
contentView.clipsToBounds = true
contentView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
......
......@@ -10,7 +10,9 @@ import Nuke
import OneWeatherCore
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 {
......@@ -40,17 +42,23 @@ class ShortsCollectionViewCell: UICollectionViewCell {
Nuke.loadImage(with: imageRequest, into: imageView) {[weak self] result in
switch result {
case .success(let imageResponse):
onMain {
self?.imageView.image = imageResponse.image
if let cachedColor = self?.delegate?.averageColor(forImage: imageResponse.image,
identifier: image.url.absoluteString) {
self?.footerView.backgroundColor = cachedColor
self?.delegate?.averageColor(forImage: imageResponse.image,
identifier: shortsItem.id,
completion:
{ avgColor, shortId in
guard shortId == shortsItem.id else {
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:
break
}
......@@ -68,6 +76,7 @@ class ShortsCollectionViewCell: UICollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.image = nil
self.footerView.backgroundColor = .clear
}
......
......@@ -8,6 +8,7 @@
import UIKit
import OneWeatherCore
import OneWeatherAnalytics
import Nuke
protocol ShortsViewDelegate: AnyObject {
func didSelectShort(at index:Int)
......@@ -15,12 +16,15 @@ protocol ShortsViewDelegate: AnyObject {
class ShortsView: UIView {
//Private
private let prefetcher = ImagePrefetcher()
private let headingLabel = UILabel()
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: ShortsCollectionViewLayout())
private let avgColorProcessQueue = OperationQueue()
private let avgColorReadQueue = OperationQueue()
private var shorts: [ShortsItem] {
return ShortsManager.shared.shorts
}
private var averageColorCache = [AnyHashable:UIColor]()
private var averageColorCache = ThreadSafeDictionary<AnyHashable, UIColor>()
//Public
weak var delegate: ShortsViewDelegate?
......@@ -28,6 +32,9 @@ class ShortsView: UIView {
init() {
super.init(frame: .zero)
//Setup queues
avgColorReadQueue.maxConcurrentOperationCount = 1
prepareCollectionView()
}
......@@ -45,12 +52,14 @@ class ShortsView: UIView {
//MARK:- Prepare
private extension ShortsView {
func prepareCollectionView() {
collectionView.isPrefetchingEnabled = true
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
collectionView.register(ShortsCollectionViewCell.self, forCellWithReuseIdentifier: ShortsCollectionViewCell.kIdentifier)
collectionView.dataSource = self
collectionView.delegate = self
// collectionView.prefetchDataSource = self
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
......@@ -60,7 +69,7 @@ private extension ShortsView {
}
}
//MARK: UICollectionView Data Source
//MARK:- UICollectionView Data Source
extension ShortsView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return shorts.count
......@@ -75,6 +84,7 @@ extension ShortsView: UICollectionViewDataSource {
}
}
//MARK:- UICollectionView Delegate
extension ShortsView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == 0 {
......@@ -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
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] {
return cachedColor
completion(cachedColor, identifier)
}
else {
if let color = image.averageColor {
self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30)
return color
}
else {
return nil
self.avgColorProcessQueue.addOperation {
if let color = forImage.averageColor {
self.averageColorCache[identifier] = color.isDarkColor ? color : color.darker(by: 30)
completion(color, identifier)
}
else {
completion(nil, identifier)
}
}
}
}
......
......@@ -92,6 +92,10 @@ public struct ShortsItem {
isLiked = true
}
public mutating func dislike() {
isLiked = false
}
public mutating func markAsViewed() {
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