Сache images one by one passing them into an array (Kingfisher) - swift

I have to cache images one by one passing them into an array.
When I configure Controller, I got array of images from API. Images I got using Animation, every 0.1 sec I got new Image. But when I got all of them, I need to use cached images instead of load them again
Some variables for help
private var imagesCount = 0
private var nextImage = 0
private var imagesArray: [String] = []
private var isAnimating = true {
didSet {
if isAnimating {
animateImage()
}
}
}
Here I configure VC, and do imagesArray = images, then I will use only my array of urls
func configureView(images: ApiImages) {
guard let images = images.images else { return }
imagesArray = images
imagesCount = images.count
imageView.setImage(imageUrl: images[nextImage])
nextImage += 1
animateImage()
}
When I start my animation. Every 0.1 I get new one image, but after the end of cycle I want to use cached images
func animateImage() {
UIView.transition(with: self.imageView, duration: 0.1, options: .transitionCrossDissolve) { [weak self] in
guard let self = self else { return }
self.imageView.setImage(imageUrl: self.imagesArray[self.nextImage])
} completion: { [weak self] _ in
guard let self = self else { return }
if self.nextImage == self.imagesCount - 1{
//here is end of cycle
self.nextImage = 0
} else {
self.nextImage += 1
}
if self.isAnimating {
self.animateImage()
}
}
}
I use kingfisher, so what options I have to put here??
extension UIImageView {
func setImage(imageUrl: String, completion: (() -> ())? = nil) {
self.kf.setImage(with: URL(string: imageUrl), options: [.transition(.fade(0.5)), .alsoPrefetchToMemory]) { result in
completion?()
}
}
}
Thank you!

Related

Youtube WkWebView blank space issue

At first, YouTube videos run well. However, when the app enters the background thread, an empty screen appears. Even if the tableview is refreshed, the video screen does not return to normal, and the app must be restarted.
class HomeTableViewCell: UITableViewCell {
var disposeBag = DisposeBag()
var data = PublishRelay<ViewPost>()
var isLoaded = false
func bind() {
data.asDriver() { _ in .never() }
.drive(onNext: { [weak self] currentPost in
guard let self = self else { return }
if self.isLoaded == true {
self.videoContainerView.cueVideo(byId: currentPost.url, startSeconds: 0, suggestedQuality: .default)
}
else {
self.videoContainerView.load(withVideoId: currentPost.url)
self.isLoaded = true
}
......
} )
.disposed(by: disposeBag)
let videoContainerView: WKYTPlayerView = {
let videoContainerView = WKYTPlayerView()
videoContainerView.translatesAutoresizingMaskIntoConstraints = false
videoContainerView.clipsToBounds = true
videoContainerView.layer.cornerRadius = 15
return videoContainerView
}()
}

Which way is right for "Strong Reference Cycles for Closures"

In Apple Swift Document
A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of that closure captures the instance.
The reCreactJavascriptTool is a property of a Controller which contains a WKWebview. The main work of the reCreactJavascriptTool is reacting with the HTML file.
private lazy var reCreactJavascriptTool: JavascriptTool = {
var reCreactJSTool = JavascriptTool()
reCreactJSTool.reJSBlock = { [unowned self] (module: String?, hareModel: HareModel?) in
if module == "hare" {
self.XXXcode
}
}
reCreactJSTool.bBlock = { [unowned self] (btnId: String?) in
self.lefckId = btnId;
self.confS()
}
reCreactJSTool.reBlock = { [unowned self] in
if (self.recordUrl?.absoluteString.count)! > 0 {
XXXXXXcode
}else {
XXXXXXcode
XXXXXXcode
}
}
return reCreactJSTool
}()
If I type the code like this :
private lazy var reCreactJavascriptTool: JavascriptTool = { [unowned self] in
// And remove all the " [unowned self] " in all Blocks
reCreactJSTool.reJSBlock = { (module: String?, hareModel: HareModel?) in
if module == "hare" {
self.XXXcode
}
}
reCreactJSTool.bBlock = { (btnId: String?) in
self.lefckId = btnId;
self.confS()
}
reCreactJSTool.reBlock = { in
if (self.recordUrl?.absoluteString.count)! > 0 {
XXXXXXcode
}else {
XXXXXXcode
XXXXXXcode
}
}
}
I don't know which code snippet is right.

Having troubles with pagination on swift collectionView

My goal is load more info when the user reaches the end of my collectionViewPokemon, but the getPokes func I'm using to achieve this is being called X amount of times, corresponding to the while statement I implemented to get the info from other endpoints. That's far from the behavior I expected.
This is how my code looks:
Service class
class Service {
let baseUrl = "https://pokeapi.co/api/v2/pokemon"
let limit = 20
var offset = 0
var isPaginating = false
func getPokes(pagination: Bool = false,
handler: #escaping ([Pokemon]) -> Void) {
let topCalls = (limit*5)-limit
let originalPokes = [Pokemon]()
let morePokemons = [Pokemon]()
var endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
if pagination {
while offset < topCalls {
isPaginating = true
offset += limit
endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
self.mainApiCall(mainList: morePokemons, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
if pagination {
self.isPaginating = true
}
case .failure(let error):
print(error)
}
}
}
} else {
self.mainApiCall(mainList: originalPokes, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
case .failure(let error):
print(error)
}
}
}
}
ViewController class (the properties)
class ViewController: UIViewController,
UICollectionViewDelegateFlowLayout,
UICollectionViewDelegate,
UICollectionViewDataSource,
UIScrollViewDelegate {
private let service = Service()
private var pagingChecker = false
var generalList = [Pokemon]()
var pokemons = [Pokemon]()
var morePokemons = [Pokemon]()
. . .
}
viewDidLoad method (this is ok so far)
override func viewDidLoad() {
super.viewDidLoad()
setViews()
service.getPokes(pagination: false) { [self] (pokes) in
pokemons = pokes
generalList += pokemons
DispatchQueue.main.async {
collectionViewPokemon?.reloadData()
}
}
}
scrollViewDidScroll method (here's the error)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = scrollView.contentOffset.y
if position > ((collectionViewPokemon!.contentSize.height) - 200 - scrollView.frame.size.height) {
guard !service.isPaginating else { return }
if !pagingChecker {
service.getPokes(pagination: true) { [self] (morePokes) in
morePokemons = morePokes
generalList += morePokemons
DispatchQueue.main.async {
collectionViewPokemon?.reloadData()
}
}
} else {
pagingChecker = true
print("Done paging.")
}
}
}
The thing is I wanna fetch just one array of pokemons at a time, but instead I'm making all the endpoint calls at the same time, and also appending them to my generalList array, who owns my collectionView datasource and all that stuff.
PD: Idk if it'll help, but here's a screenshot of my console. The first msg is ok, but it should print the others only when the user reaches the end of the CollectionView, and well, that's not happening (instead, they're all coming back at the same time!)
I can't figure out how to get this done. I would really appreciate any advice or correction, thanks.
I figured out what was happening. I was using a while statement to repeat my Api call as long as it satisfy it's condition (limit<topCalls).
So, I worked out some ways to not using that while statement, and ended up using a function to construct every URL, and then, set up a shouldPage parameter that checked if that apiCall needed to be done or not.
This is how my code ended up:
let baseUrl = "https://pokeapi.co/api/v2/pokemon"
let limit = 20
var offset = 0
func getPokes(pageNumber: Int = 1,
shouldPage: Bool = true,
handler: #escaping ([Pokemon]) -> Void) {
let originalPokes = [Pokemon]()
let morePokemons = [Pokemon]()
let endpoint = buildEndpoint(shouldPage: shouldPage)
if shouldPage {
mainApiCall(mainList: morePokemons, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
case .failure(let error):
print(error)
}
}
} else {
mainApiCall(mainList: originalPokes, endpoint: endpoint) { (result) in
switch result {
case .success(let pokemons):
handler(pokemons)
case .failure(let error):
print(error)
}
}
}
}
private func buildEndpoint(shouldPage: Bool = false) -> String {
var endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
if shouldPage {
offset += limit
endpoint = "\(baseUrl)/?limit=\(limit)&offset=\(offset)"
}
return endpoint
}
That's on my Service class. So, in my CollectionView Datasource, I call getPokes(shouldPage: false) on cellForItemAt, and in willDisplay cell I say shouldPage is true. That fixed the problem.

Swift: Notification permissions pop up affecting class functionality

This is a hard one to explain as I'm not entirely sure what's going wrong here. In my AppDelegate I add an array of services to the app:
override var services: [ApplicationService] {
if UIApplication.isRunningUnitTests() {
return []
}
return [
SettingsService(),
TrackingApplicationService(),
NotificationApplicationService(),
AudioApplicationService(),
ConnectionService()
]
}
The connection service is the one I am interested in here. It looks like this:
import PluggableAppDelegate
import Connectivity
final class ConnectionService: NSObject, ApplicationService {
// MARK: - Helpers
private struct Constants {
static let containerHeight: CGFloat = 20
static let containerPadding: CGFloat = 20
static let statusCenterAdjustment: CGFloat = -1
static let statusFont: UIFont = .systemFont(ofSize: 15, weight: .semibold)
static let labelLines = 1
static let radiusDenominator: CGFloat = 2.0
static let animationDuration = 0.2
static let defaultAnchorConstant: CGFloat = 0
}
internal enum ConnectionStatus {
case connected
case disconnected
}
// MARK: - Properties
private let connectivity = Connectivity()
private let queue = DispatchQueue.global(qos: .utility)
internal var currentStatus = ConnectionStatus.connected
private var statusTopAnchor: NSLayoutConstraint?
private lazy var statusContainer: UIView = {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = UIColor.red
container.layer.cornerRadius = Constants.containerHeight / Constants.radiusDenominator
container.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]
container.isHidden = true
container.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor, constant: Constants.statusCenterAdjustment),
statusLabel.leftAnchor.constraint(equalTo: container.leftAnchor, constant: Constants.containerPadding),
statusLabel.rightAnchor.constraint(equalTo: container.rightAnchor, constant: -Constants.containerPadding)
])
return container
}()
private lazy var statusLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = Constants.labelLines
label.font = Constants.statusFont
label.textAlignment = .center
label.textColor = .white
label.text = Strings.common.user.noInternetConnection.localized
return label
}()
// MARK: - Lifecycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
DispatchQueue.main.async { [weak self] in
self?.prepareUI()
}
prepareConnectivityListener()
updateConnectionStatus(connectivity.status)
return true
}
// MARK: - Connectivity
func updateConnectionStatus(_ status: Connectivity.Status) {
switch status {
case .connected, .connectedViaWiFi, .connectedViaCellular:
updateLabelWith(connectionStatus: .connected)
case .notConnected, .connectedViaWiFiWithoutInternet, .connectedViaCellularWithoutInternet:
updateLabelWith(connectionStatus: .disconnected)
}
NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
}
// MARK: - Configuration
private func prepareUI() {
guard let window = UIApplication.shared.keyWindow else {
assertionFailure("We require a Window")
return // Don't setup in release if we hit this, no point
}
window.addSubview(statusContainer)
statusTopAnchor = statusContainer.topAnchor.constraint(equalTo: window.topAnchor, constant: -Constants.containerHeight)
guard let statusTopAnchor = statusTopAnchor else {
assertionFailure("Top anchor could not be build")
return
}
NSLayoutConstraint.activate([
statusTopAnchor,
statusContainer.centerXAnchor.constraint(equalTo: window.centerXAnchor),
statusContainer.heightAnchor.constraint(equalToConstant: Constants.containerHeight)
])
}
private func prepareConnectivityListener() {
let connectivityChanged: (Connectivity) -> Void = { [weak self] connectivity in
self?.updateConnectionStatus(connectivity.status)
}
connectivity.whenConnected = connectivityChanged
connectivity.whenDisconnected = connectivityChanged
connectivity.startNotifier(queue: queue)
}
private func updateLabelWith(connectionStatus: ConnectionStatus) {
if currentStatus == connectionStatus {
return // No need to update if we are the same state
} else {
currentStatus = connectionStatus
}
let topAnchorConstant: CGFloat = (connectionStatus == .connected) ? -Constants.containerHeight : Constants.defaultAnchorConstant
DispatchQueue.main.async { [weak self] in
DispatchQueue.main.async { [weak self] in
if connectionStatus != .connected {
self?.statusContainer.isHidden = false
}
self?.statusTopAnchor?.constant = topAnchorConstant
UIView.animate(withDuration: Constants.animationDuration,
animations: { [weak self] in
self?.statusContainer.superview?.layoutIfNeeded()
},
completion: { _ in
if connectionStatus == .connected {
self?.statusContainer.isHidden = true
}
})
}
}
}
}
So I'm using this service to check the connection status of the device and on the login view controller I display different warnings depending on the connection status.
However, my problem is is that the connection service is not functioning properly when the permissions pop up appears. As you can see from the ConnectionService class, an alert should be generated when the app is offline. Now because I am adding this connection service before the permissions are requested, the window briefly shows the alert, but as soon as the permissions alert appears, it disappears.
Furthermore, you can see in ConnectionService.swift I have a listener set up:
NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
I'm listening for this in the subsequent viewController which triggers this method:
#objc private func connectivityChanged(notification: Notification) {
if notification.object as? ConnectionService.ConnectionStatus == .connected {
deviceConnected = true
} else {
deviceConnected = false
}
}
This in turn triggers a UI change:
private var deviceConnected: Bool = true {
willSet {
DispatchQueue.main.async {
if newValue == true {
self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_completed")
self.errorLabel?.isHidden = true
} else {
self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_offline")
self.errorLabel?.isHidden = false
self.errorLabel?.text = Strings.eHandshake.warning.deviceOffline.title.localized
}
}
}
}
When permissions have already been granted and therefore the app launches without requesting permission, this works fine, but when the permissions alert appears, this seems to break this part of the logic.
To be clear the services var is initially declared here:
public protocol ApplicationService: UIApplicationDelegate {}
extension ApplicationService {
public var window: UIWindow? {
return UIApplication.shared.delegate?.window ?? nil
}
}
open class PluggableApplicationDelegate: UIResponder,
UIApplicationDelegate {
public var window: UIWindow?
open var services: [ApplicationService] { return [] }
internal lazy var _services: [ApplicationService] = {
return self.services
}()
#discardableResult
internal func apply<T, S>(_ work: (ApplicationService, #escaping (T) -> Void) -> S?, completionHandler: #escaping ([T]) -> Swift.Void) -> [S] {
let dispatchGroup = DispatchGroup()
var results: [T] = []
var returns: [S] = []
for service in _services {
dispatchGroup.enter()
let returned = work(service, { result in
results.append(result)
dispatchGroup.leave()
})
if let returned = returned {
returns.append(returned)
} else { // delegate doesn't implement method
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
completionHandler(results)
}
return returns
}
}
Any help with this would be much appreciated.

CANT RESOLVE: unsafeAddressOf is abandoned in Swift 3

I just realized that my old app is not working anymore because unsafeAddressOf is abandoned in Swift 3. I have been searching in Apple documentations and online tutorials but still cant figure out how to change my code to be compliant with Swift 3. Here is my code:
import UIKit
import ImageIO
extension UIImage {
public class func gifWithData(data: NSData) -> UIImage? {
guard let source = CGImageSourceCreateWithData(data, nil) else {
print("SwiftGif: Source for the image does not exist")
return nil
}
return UIImage.animatedImageWithSource(source: source)
}
public class func gifWithName(name: String) -> UIImage? {
guard let bundleURL = Bundle.main.url(forResource: name, withExtension: "gif") else {
print("SwiftGif: This image named \"\(name)\" does not exist")
return nil
}
guard let imageData = NSData(contentsOfURL: bundleURL) else {
print("SwiftGif: Cannot turn image named \"\(name)\" into NSData")
return nil
}
return gifWithData(imageData)
}
class func delayForImageAtIndex(index: Int, source: CGImageSource!) -> Double {
var delay = 0.1
// Get dictionaries
let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
let gifProperties: CFDictionary = unsafeBitCast(CFDictionaryGetValue(cfProperties, unsafeAddressOf(kCGImagePropertyGIFDictionary)), to: CFDictionary.self)
// Get delay time
var delayObject: AnyObject = unsafeBitCast(
CFDictionaryGetValue(gifProperties,
unsafeAddressOf(kCGImagePropertyGIFUnclampedDelayTime)),
to: AnyObject.self)
if delayObject.doubleValue == 0 {
delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties,
unsafeAddressOf(kCGImagePropertyGIFDelayTime)), to: AnyObject.self)
}
delay = delayObject as! Double
if delay < 0.1 {
delay = 0.1 // Make sure they're not too fast
}
return delay
}
class func gcdForPair( a: Int?, var _ b: Int?) -> Int {
// Check if one of them is nil
var a = a
if b == nil || a == nil {
if b != nil {
return b!
} else if a != nil {
return a!
} else {
return 0
}
}
// Swap for modulo
if a < b {
let c = a
a = b
b = c
}
// Get greatest common divisor
var rest: Int
while true {
rest = a! % b!
if rest == 0 {
return b! // Found it
} else {
a = b
b = rest
}
}
}
class func gcdForArray(array: Array<Int>) -> Int {
if array.isEmpty {
return 1
}
var gcd = array[0]
for val in array {
gcd = UIImage.gcdForPair(val, gcd)
}
return gcd
}
class func animatedImageWithSource(source: CGImageSource) -> UIImage? {
let count = CGImageSourceGetCount(source)
var images = [CGImage]()
var delays = [Int]()
// Fill arrays
for i in 0..<count {
// Add image
if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
images.append(image)
}
// At it's delay in cs
let delaySeconds = UIImage.delayForImageAtIndex(index: Int(i),
source: source)
delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
}
// Calculate full duration
let duration: Int = {
var sum = 0
for val: Int in delays {
sum += val
}
return sum
}()
// Get frames
let gcd = gcdForArray(array: delays)
var frames = [UIImage]()
var frame: UIImage
var frameCount: Int
for i in 0..<count {
frame = UIImage(CGImage: images[Int(i)])
frameCount = Int(delays[Int(i)] / gcd)
for _ in 0..<frameCount {
frames.append(frame)
}
}
// Heyhey
let animation = UIImage.animatedImage(with: frames,
duration: Double(duration) / 1000.0)
return animation
}
}
Does anyone have an idea how I can fix this code?