Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f6ff38a71 | |||
| e072b40ea5 | |||
| de1e60ee6b | |||
| f469bd70ab | |||
| 8d19bad3e9 | |||
| 689f0b1bfb | |||
| fcdf49d4a4 | |||
| 52b001769e | |||
| 09aa06bec1 | |||
| 35c8f76b22 | |||
| 249fd977e3 | |||
| f61639a53f | |||
| cebff16524 | |||
| 03d73775f2 | |||
| 3a45fcc386 | |||
| efc710363e | |||
| 47730019ad | |||
| 1827c4686e | |||
| c004cbf227 |
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |spec|
|
||||
|
||||
spec.name = "Glideshow"
|
||||
spec.version = "1.0.0"
|
||||
spec.version = "1.1.1"
|
||||
spec.summary = "A slideshow written in Swift 5 that adds transitions to labels within a slide"
|
||||
|
||||
spec.description = <<-DESC
|
||||
@@ -12,8 +12,8 @@ Pod::Spec.new do |spec|
|
||||
spec.license = { :type => "MIT", :file => "LICENSE" }
|
||||
spec.author = { "Visal Rajapakse" => "visalrajapakse@gmail.com" }
|
||||
|
||||
spec.platform = :ios, "13.0"
|
||||
spec.ios.deployment_target = "13.0"
|
||||
spec.platform = :ios, "11.0"
|
||||
spec.ios.deployment_target = "11.0"
|
||||
spec.swift_version = "5"
|
||||
|
||||
spec.source = { :git => "https://github.com/v15a1/Glideshow.git", :tag => "#{spec.version}" }
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
4B0CF40E25EBD50F005975B1 /* Glideshow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B0739E625EB56BD003FF9C1 /* Glideshow.framework */; };
|
||||
4B0CF40F25EBD50F005975B1 /* Glideshow.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4B0739E625EB56BD003FF9C1 /* Glideshow.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
4B0CF41A25EBD590005975B1 /* GlideCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0CF41925EBD590005975B1 /* GlideCell.swift */; };
|
||||
4B3447F525F7CD6D00A713F2 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3447F425F7CD6D00A713F2 /* ImageCache.swift */; };
|
||||
4BB22DFB25F7D35800B6FD9D /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB22DFA25F7D35800B6FD9D /* ImageLoader.swift */; };
|
||||
6E82188B2666B6450012A2CD /* GlideCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E82188A2666B6450012A2CD /* GlideCellTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -74,6 +77,9 @@
|
||||
4B0CF40325EB760D005975B1 /* GlideLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlideLabel.swift; sourceTree = "<group>"; };
|
||||
4B0CF40825EB76BD005975B1 /* GlideItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlideItem.swift; sourceTree = "<group>"; };
|
||||
4B0CF41925EBD590005975B1 /* GlideCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlideCell.swift; sourceTree = "<group>"; };
|
||||
4B3447F425F7CD6D00A713F2 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||
4BB22DFA25F7D35800B6FD9D /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = "<group>"; };
|
||||
6E82188A2666B6450012A2CD /* GlideCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlideCellTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -131,6 +137,8 @@
|
||||
4B0739EA25EB56BD003FF9C1 /* Info.plist */,
|
||||
4B073A1D25EB5955003FF9C1 /* Glideshow.swift */,
|
||||
4B0CF41925EBD590005975B1 /* GlideCell.swift */,
|
||||
4B3447F425F7CD6D00A713F2 /* ImageCache.swift */,
|
||||
4BB22DFA25F7D35800B6FD9D /* ImageLoader.swift */,
|
||||
4B0CF40325EB760D005975B1 /* GlideLabel.swift */,
|
||||
4B0CF40825EB76BD005975B1 /* GlideItem.swift */,
|
||||
);
|
||||
@@ -142,6 +150,7 @@
|
||||
children = (
|
||||
4B0739F425EB56BD003FF9C1 /* GlideshowTests.swift */,
|
||||
4B0739F625EB56BD003FF9C1 /* Info.plist */,
|
||||
6E82188A2666B6450012A2CD /* GlideCellTests.swift */,
|
||||
);
|
||||
path = GlideshowTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -309,7 +318,9 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4BB22DFB25F7D35800B6FD9D /* ImageLoader.swift in Sources */,
|
||||
4B0CF40425EB760D005975B1 /* GlideLabel.swift in Sources */,
|
||||
4B3447F525F7CD6D00A713F2 /* ImageCache.swift in Sources */,
|
||||
4B0CF40925EB76BD005975B1 /* GlideItem.swift in Sources */,
|
||||
4B073A1E25EB5955003FF9C1 /* Glideshow.swift in Sources */,
|
||||
4B0CF41A25EBD590005975B1 /* GlideCell.swift in Sources */,
|
||||
@@ -321,6 +332,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4B0739F525EB56BD003FF9C1 /* GlideshowTests.swift in Sources */,
|
||||
6E82188B2666B6450012A2CD /* GlideCellTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -503,7 +515,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Glideshow/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -530,7 +542,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = Glideshow/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -590,7 +602,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = FVZ5PWG6VV;
|
||||
INFOPLIST_FILE = GlideshowExample/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -610,7 +622,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = FVZ5PWG6VV;
|
||||
INFOPLIST_FILE = GlideshowExample/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
//MARK: Protocol - GlideableCellDelegate
|
||||
/// Protocol to inform cell about scroll state
|
||||
@@ -54,30 +55,6 @@ class GlideCell: UICollectionViewCell {
|
||||
|
||||
// Protocol variable to hide/show labels based on value
|
||||
public var isProminent: Bool = true
|
||||
|
||||
/// Value of the slide title
|
||||
public var title : String? {
|
||||
didSet{
|
||||
slideTitle.text = title
|
||||
layoutSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
/// Value of the slide description
|
||||
public var story : String?{
|
||||
didSet{
|
||||
slideDescription.text = story
|
||||
layoutSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
/// Value of the slide caption
|
||||
public var caption : String?{
|
||||
didSet{
|
||||
slideCaption.text = caption
|
||||
layoutSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up slide background image if image is available
|
||||
public var backgroundImage : UIImage? {
|
||||
@@ -163,7 +140,7 @@ class GlideCell: UICollectionViewCell {
|
||||
}()
|
||||
|
||||
/// Spacing between labels of the slide. Default value : 8
|
||||
public var labelSpacing : CGFloat!
|
||||
public var labelSpacing : CGFloat = 8
|
||||
|
||||
/// Animateable GlideLabels for gliding
|
||||
public var animateableViews : [GlideLabel]?
|
||||
@@ -175,14 +152,14 @@ class GlideCell: UICollectionViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// glide factor for the description lable. Default: 2
|
||||
/// glide factor for the description lable. Default: 3
|
||||
public var descriptionGlideFactor : CGFloat = 3 {
|
||||
didSet{
|
||||
slideDescription.glideFactor = titleGlideFactor
|
||||
}
|
||||
}
|
||||
|
||||
/// glide factor for the title lable. Default: 2
|
||||
/// glide factor for the title lable. Default: 1
|
||||
public var captionGlideFactor : CGFloat = 1 {
|
||||
didSet{
|
||||
slideCaption.glideFactor = titleGlideFactor
|
||||
@@ -199,6 +176,9 @@ class GlideCell: UICollectionViewCell {
|
||||
|
||||
/// Maximum width of a GlideLabel. Calculated using leading inset of cell
|
||||
private var animateableMaxWidth : CGFloat!
|
||||
|
||||
@available(iOS 13, *)
|
||||
private lazy var cancellable : AnyCancellable? = nil
|
||||
|
||||
public override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
@@ -215,6 +195,18 @@ class GlideCell: UICollectionViewCell {
|
||||
initialize()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
if backgroundImage != nil {
|
||||
imageView.image = nil
|
||||
}
|
||||
if #available(iOS 13, *) {
|
||||
cancellable?.cancel()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Setting up cell
|
||||
@@ -233,6 +225,52 @@ class GlideCell: UICollectionViewCell {
|
||||
slide.clipsToBounds = true
|
||||
}
|
||||
|
||||
/// Configures cell
|
||||
/// - Parameter item: `GlideItem` to configure cell with
|
||||
public func configure( with item : GlideItem, placeholderImage : UIImage?){
|
||||
slideCaption.text = item.caption
|
||||
slideTitle.text = item.title
|
||||
slideDescription.text = item.description
|
||||
if let bgImage = item.backgroundImage{
|
||||
backgroundImage = bgImage
|
||||
}else{
|
||||
backgroundImage = placeholderImage
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
cancellable = loadImage(for: item).sink{
|
||||
[weak self] image in self?.showNetworkImage(for: image)
|
||||
}
|
||||
} else {
|
||||
imageView.loadImage(urlString: item.imgURL!)
|
||||
}
|
||||
}
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
|
||||
/// Displays retrieved image
|
||||
/// - Parameter image: Image to display
|
||||
private func showNetworkImage(for image : UIImage?) {
|
||||
UIView.transition(with: self, duration: 0.3, options: .transitionCrossDissolve, animations: {
|
||||
self.backgroundImage = image
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
/// Caches loaded image
|
||||
/// - Parameter item: `GlideItem` to retrieve URL
|
||||
/// - Returns: Returns `Just` publisher with the cached image if any.
|
||||
@available(iOS 13.0, *)
|
||||
private func loadImage(for item: GlideItem) -> AnyPublisher<UIImage?, Never> {
|
||||
return Just(item.imgURL)
|
||||
.flatMap({ poster -> AnyPublisher<UIImage?, Never> in
|
||||
let url = URL(string: item.imgURL ?? "")!
|
||||
return ImageLoader.shared.loadImage(from: url)
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
/// Setup Description, Title, Caption in stated order by positioning based on each Glidelabels content
|
||||
private func setupAnimateables(){
|
||||
//setting up animateables
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
public class GlideItem {
|
||||
public struct GlideItem {
|
||||
|
||||
/// Caption of the slide
|
||||
public var caption : String!
|
||||
@@ -21,6 +21,8 @@ public class GlideItem {
|
||||
/// Asset image for slide background
|
||||
public var backgroundImage : UIImage?
|
||||
|
||||
public var imgURL : String?
|
||||
|
||||
/// Initializer for GlideItem
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -39,4 +41,23 @@ public class GlideItem {
|
||||
self.description = description
|
||||
self.backgroundImage = backgroundImage
|
||||
}
|
||||
|
||||
/// Initializer for GlideItem
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - caption: Caption of the slide
|
||||
/// - title: Title of the slide
|
||||
/// - description: Description of the slide
|
||||
/// - imageURL: Image URL to load
|
||||
public init(
|
||||
caption : String! = nil,
|
||||
title : String! = nil,
|
||||
description : String! = nil,
|
||||
imageURL : String! = nil
|
||||
){
|
||||
self.caption = caption
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.imgURL = imageURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
// Magic number used to give the user the feeling they are able to swipe infinitely
|
||||
fileprivate let kInifiniteSwipingMultiplier = 55
|
||||
|
||||
@objc
|
||||
/// The delegate protocol that notifies slideshow state changes
|
||||
public protocol GlideshowProtocol : AnyObject {
|
||||
@@ -102,6 +105,8 @@ public class Glideshow: UIView {
|
||||
/// Default slide color
|
||||
public var defaultSlideColor : UIColor = UIColor.lightGray
|
||||
|
||||
public var placeHolderImage : UIImage?
|
||||
|
||||
/// Spacing between `SlideLabels` embedded in the slide
|
||||
public var labelSpacing : CGFloat = 8
|
||||
|
||||
@@ -401,7 +406,6 @@ public class Glideshow: UIView {
|
||||
|
||||
collectionView?.frame = self.bounds
|
||||
case .bottom:
|
||||
print("bottom")
|
||||
slideMargin.bottom = 10
|
||||
collectionView?.frame = CGRect(x: 0, y: 0, width: width, height: self.frame.height - 10)
|
||||
pageIndicator!.frame = CGRect(x: 0, y: self.frame.height - 10, width: width, height: 10)
|
||||
@@ -462,7 +466,7 @@ public class Glideshow: UIView {
|
||||
extension Glideshow : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return (items?.count ?? 0) * (isCircular ? 55 : 1)
|
||||
return (items?.count ?? 0) * (isCircular ? kInifiniteSwipingMultiplier : 1)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
@@ -497,10 +501,10 @@ extension Glideshow : UICollectionViewDelegate, UICollectionViewDataSource, UICo
|
||||
}
|
||||
/// Cell setup for passed `GlideItems`
|
||||
if let item = items?[indexPath.item % (items?.count ?? 0)] {
|
||||
cell.caption = item.caption
|
||||
cell.title = item.title
|
||||
cell.story = item.description
|
||||
cell.backgroundImage = item.backgroundImage
|
||||
cell.configure(
|
||||
with: item,
|
||||
placeholderImage: placeHolderImage
|
||||
)
|
||||
}
|
||||
|
||||
return cell
|
||||
@@ -572,6 +576,7 @@ extension Glideshow : UICollectionViewDelegate, UICollectionViewDataSource, UICo
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
delegate?.glideshowDidSelecteRowAt?(indexPath: indexPath, self)
|
||||
let index = IndexPath(item: ((pageByMidSection) % items!.count), section: 0)
|
||||
delegate?.glideshowDidSelecteRowAt?(indexPath: index, self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// ImageCache.swift
|
||||
// Glideshow
|
||||
//
|
||||
// Created by Visal Rajapakse on 2021-03-09.
|
||||
//
|
||||
// Referenced from Maksym Shcheglov
|
||||
// https://medium.com/flawless-app-stories/reusable-image-cache-in-swift-9b90eb338e8d
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit.UIImage
|
||||
import Combine
|
||||
|
||||
// Declares in-memory image cache
|
||||
public protocol ImageCacheType: class {
|
||||
// Returns the image associated with a given url
|
||||
func image(for url: URL) -> UIImage?
|
||||
// Inserts the image of the specified url in the cache
|
||||
func insertImage(_ image: UIImage?, for url: URL)
|
||||
// Removes the image of the specified url in the cache
|
||||
func removeImage(for url: URL)
|
||||
// Removes all images from the cache
|
||||
func removeAllImages()
|
||||
// Accesses the value associated with the given key for reading and writing
|
||||
subscript(_ url: URL) -> UIImage? { get set }
|
||||
}
|
||||
|
||||
public class ImageCache: ImageCacheType {
|
||||
|
||||
// 1st level cache, that contains encoded images
|
||||
lazy var imageCache: NSCache<AnyObject, AnyObject> = {
|
||||
let cache = NSCache<AnyObject, AnyObject>()
|
||||
cache.countLimit = config.countLimit
|
||||
return cache
|
||||
}()
|
||||
// 2nd level cache, that contains decoded images
|
||||
lazy var decodedImageCache: NSCache<AnyObject, AnyObject> = {
|
||||
let cache = NSCache<AnyObject, AnyObject>()
|
||||
cache.totalCostLimit = config.memoryLimit
|
||||
return cache
|
||||
}()
|
||||
|
||||
private let lock = NSLock()
|
||||
private let config: Config
|
||||
|
||||
public struct Config {
|
||||
public let countLimit: Int
|
||||
public let memoryLimit: Int
|
||||
|
||||
public static let defaultConfig = Config(countLimit: 100, memoryLimit: 1024 * 1024 * 100) // 100 MB
|
||||
}
|
||||
|
||||
public init(config: Config = Config.defaultConfig) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
public func image(for url: URL) -> UIImage? {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
// the best case scenario -> there is a decoded image in memory
|
||||
if let decodedImage = decodedImageCache.object(forKey: url as AnyObject) as? UIImage {
|
||||
return decodedImage
|
||||
}
|
||||
// search for image data
|
||||
if let image = imageCache.object(forKey: url as AnyObject) as? UIImage {
|
||||
let decodedImage = image.decodedImage()
|
||||
decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decodedImage.diskSize)
|
||||
return decodedImage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func insertImage(_ image: UIImage?, for url: URL) {
|
||||
guard let image = image else { return removeImage(for: url) }
|
||||
let decompressedImage = image.decodedImage()
|
||||
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
imageCache.setObject(decompressedImage, forKey: url as AnyObject, cost: 1)
|
||||
decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decompressedImage.diskSize)
|
||||
}
|
||||
|
||||
public func removeImage(for url: URL) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
imageCache.removeObject(forKey: url as AnyObject)
|
||||
decodedImageCache.removeObject(forKey: url as AnyObject)
|
||||
}
|
||||
|
||||
public func removeAllImages() {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
imageCache.removeAllObjects()
|
||||
decodedImageCache.removeAllObjects()
|
||||
}
|
||||
|
||||
public subscript(_ key: URL) -> UIImage? {
|
||||
get {
|
||||
return image(for: key)
|
||||
}
|
||||
set {
|
||||
return insertImage(newValue, for: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension UIImage {
|
||||
|
||||
func decodedImage() -> UIImage {
|
||||
guard let cgImage = cgImage else { return self }
|
||||
let size = CGSize(width: cgImage.width, height: cgImage.height)
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: cgImage.bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
|
||||
context?.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
||||
guard let decodedImage = context?.makeImage() else { return self }
|
||||
return UIImage(cgImage: decodedImage)
|
||||
}
|
||||
|
||||
// Rough estimation of how much memory image uses in bytes
|
||||
var diskSize: Int {
|
||||
guard let cgImage = cgImage else { return 0 }
|
||||
return cgImage.bytesPerRow * cgImage.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ImageLoader.swift
|
||||
// Glideshow
|
||||
//
|
||||
// Created by Visal Rajapakse on 2021-03-09.
|
||||
//
|
||||
// Referenced from Maksym Shcheglov
|
||||
// https://medium.com/flawless-app-stories/reusable-image-cache-in-swift-9b90eb338e8d
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit.UIImage
|
||||
import Combine
|
||||
|
||||
public final class ImageLoader {
|
||||
public static let shared = ImageLoader()
|
||||
|
||||
private let imageCache: ImageCacheType
|
||||
private lazy var backgroundQueue: OperationQueue = {
|
||||
let queue = OperationQueue()
|
||||
queue.maxConcurrentOperationCount = 5
|
||||
return queue
|
||||
}()
|
||||
|
||||
public init(cache: ImageCacheType = ImageCache()) {
|
||||
self.imageCache = cache
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func loadImage(from url: URL) -> AnyPublisher<UIImage?, Never> {
|
||||
if let image = imageCache[url] {
|
||||
return Just(image).eraseToAnyPublisher()
|
||||
}
|
||||
return URLSession.shared.dataTaskPublisher(for: url)
|
||||
.map { (data, response) -> UIImage? in return UIImage(data: data) }
|
||||
.catch { error in return Just(nil) }
|
||||
.handleEvents(receiveOutput: {[unowned self] image in
|
||||
guard let image = image else { return }
|
||||
self.imageCache[url] = image
|
||||
})
|
||||
.subscribe(on: backgroundQueue)
|
||||
.receive(on: RunLoop.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
var cache = NSCache<NSString, UIImage>()
|
||||
|
||||
extension UIImageView {
|
||||
|
||||
func loadImage(urlString: String) {
|
||||
|
||||
if let cacheImage = cache.object(forKey: urlString as NSString) {
|
||||
self.image = cacheImage
|
||||
return
|
||||
}
|
||||
guard let url = URL(string: urlString) else { return }
|
||||
|
||||
URLSession.shared.dataTask(with: url) { (data, response, error) in
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else { return }
|
||||
let image = UIImage(data: data)
|
||||
cache.setObject(image!, forKey: urlString as NSString)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.image = image
|
||||
}
|
||||
}.resume()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "dish_default_image.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "placeholder-image.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 30 KiB |
@@ -7,6 +7,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
@@ -29,19 +29,22 @@ class ViewController: UIViewController {
|
||||
view.backgroundColor = .white
|
||||
|
||||
glideshow.items = [
|
||||
GlideItem(caption : "Привет", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "d42fdbf73868af1f844b88a30617f9d7")),
|
||||
GlideItem(caption : "Hello in Japanese",title : "こんにちは", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "0805fe161e1e1089d916c9286696d5eb")),
|
||||
GlideItem(caption : "Hello in Chinese",title : "你好", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "42b5b97f3a2dea3b4a860b7786f628ad")),
|
||||
GlideItem(caption : "Hello in Thai",title : "สวัสดี", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "1ad7f0c78859a871347dd732cfd2b76e"))
|
||||
GlideItem(caption : "Привет", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", imageURL: "https://picsum.photos/1080/720"),
|
||||
GlideItem(caption : "Hello in Japanese",title : "こんにちは", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "image10")),
|
||||
GlideItem(caption : "Hello in Chinese",title : "你好", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "image7")),
|
||||
GlideItem(caption : "Hello in Thai",title : "สวัสดี", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor sit amet, consectetur adipiscing elit.", backgroundImage: #imageLiteral(resourceName: "image9"))
|
||||
]
|
||||
glideshow.delegate = self
|
||||
glideshow.isCircular = true
|
||||
glideshow.placeHolderImage = #imageLiteral(resourceName: "dish_default_image")
|
||||
glideshow.gradientColor =
|
||||
UIColor.black.withAlphaComponent(0.8)
|
||||
glideshow.captionFont = UIFont.systemFont(ofSize: 16, weight: .light)
|
||||
glideshow.titleFont = UIFont.systemFont(ofSize: 30, weight: .black)
|
||||
glideshow.gradientHeightFactor = 0.8
|
||||
glideshow.pageIndicatorPosition = .bottom
|
||||
glideshow.interval = 2
|
||||
glideshow.interval = 5
|
||||
|
||||
button.setTitle("Go to item number 3", for: .normal)
|
||||
button.addTarget(self, action: #selector(goToSlide), for: .touchUpInside)
|
||||
|
||||
@@ -55,3 +58,13 @@ class ViewController: UIViewController {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController : GlideshowProtocol {
|
||||
func glideshowDidSelecteRowAt(indexPath: IndexPath, _ glideshow: Glideshow) {
|
||||
print(indexPath)
|
||||
}
|
||||
|
||||
func pageDidChange(_ glideshow: Glideshow, didChangePageTo page: Int) {
|
||||
print(page)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// GlideCellTests.swift
|
||||
// GlideshowTests
|
||||
//
|
||||
// Created by Jesus Andres Bernal Lopez on 6/1/21.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Glideshow
|
||||
|
||||
class GlideCellTests: XCTestCase {
|
||||
|
||||
func test_configureWithBackgroundImage() {
|
||||
let cell = GlideCell()
|
||||
|
||||
let testItem = GlideItem(caption: "a-caption", title: "a-title", description: "a-description", backgroundImage: UIImage.checkmark)
|
||||
|
||||
cell.configure(with: testItem, placeholderImage: nil)
|
||||
|
||||
XCTAssertEqual(cell.slideCaption.text, testItem.caption)
|
||||
XCTAssertEqual(cell.slideTitle.text, testItem.title)
|
||||
XCTAssertEqual(cell.slideDescription.text, testItem.description)
|
||||
XCTAssertEqual(cell.backgroundImage, testItem.backgroundImage)
|
||||
}
|
||||
|
||||
func test_configureWithImageURL() {
|
||||
let cell = GlideCell()
|
||||
|
||||
let testItem = GlideItem(caption: "a-caption", title: "a-title", description: "a-description", imageURL: "a-url")
|
||||
|
||||
let placeholderImage = UIImage.checkmark
|
||||
|
||||
cell.configure(with: testItem, placeholderImage: placeholderImage)
|
||||
|
||||
XCTAssertEqual(cell.slideCaption.text, testItem.caption)
|
||||
XCTAssertEqual(cell.slideTitle.text, testItem.title)
|
||||
XCTAssertEqual(cell.slideDescription.text, testItem.description)
|
||||
XCTAssertEqual(cell.backgroundImage, placeholderImage)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,4 +13,102 @@ class GlideshowTests: XCTestCase {
|
||||
override func setUp() {
|
||||
glideshow = Glideshow()
|
||||
}
|
||||
|
||||
func test_interval() {
|
||||
XCTAssertEqual(glideshow.interval, 0)
|
||||
}
|
||||
|
||||
func test_slideMargin() {
|
||||
XCTAssertEqual(glideshow.slideMargin, UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20))
|
||||
}
|
||||
|
||||
func test_slidePadding() {
|
||||
XCTAssertEqual(glideshow.slidePadding, UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20))
|
||||
}
|
||||
|
||||
func test_defaultSldeColor() {
|
||||
XCTAssertEqual(glideshow.defaultSlideColor, .lightGray)
|
||||
}
|
||||
|
||||
func test_placeHolderImage() {
|
||||
XCTAssertEqual(glideshow.placeHolderImage, nil)
|
||||
}
|
||||
|
||||
func test_labelSpacing() {
|
||||
XCTAssertEqual(glideshow.labelSpacing, 8)
|
||||
}
|
||||
|
||||
func test_isGradientEnabled() {
|
||||
XCTAssertEqual(glideshow.isGradientEnabled, false)
|
||||
}
|
||||
|
||||
func test_titleFont() {
|
||||
XCTAssertEqual(glideshow.titleFont, UIFont.systemFont(ofSize: 20, weight: .black))
|
||||
}
|
||||
|
||||
func test_descriptionGlideFactor() {
|
||||
XCTAssertEqual(glideshow.descriptionGlideFactor, 3)
|
||||
}
|
||||
|
||||
func test_titleGlideFactor() {
|
||||
XCTAssertEqual(glideshow.titleGlideFactor, 2)
|
||||
}
|
||||
|
||||
func test_captionGlideFactor() {
|
||||
XCTAssertEqual(glideshow.captionGlideFactor, 1)
|
||||
}
|
||||
|
||||
func test_descriptionFont() {
|
||||
XCTAssertEqual(glideshow.descriptionFont, UIFont.systemFont(ofSize: 16, weight: .regular))
|
||||
}
|
||||
|
||||
func test_captionFont() {
|
||||
XCTAssertEqual(glideshow.captionFont, UIFont.systemFont(ofSize: 14, weight: .light))
|
||||
}
|
||||
|
||||
func test_titleColor() {
|
||||
XCTAssertEqual(glideshow.titleColor, .white)
|
||||
}
|
||||
|
||||
func test_descriptionColor() {
|
||||
XCTAssertEqual(glideshow.descriptionColor, .white)
|
||||
}
|
||||
|
||||
func test_captionColor() {
|
||||
XCTAssertEqual(glideshow.captionColor, .white)
|
||||
}
|
||||
|
||||
func test_gradientColor() {
|
||||
XCTAssertEqual(glideshow.gradientColor, UIColor.black.withAlphaComponent(0.6))
|
||||
XCTAssertEqual(glideshow.isGradientEnabled, false)
|
||||
|
||||
glideshow.gradientColor = .orange
|
||||
|
||||
XCTAssertEqual(glideshow.gradientColor, .orange)
|
||||
XCTAssertEqual(glideshow.isGradientEnabled, true)
|
||||
}
|
||||
|
||||
func test_gradientHeightFactor() {
|
||||
XCTAssertEqual(glideshow.gradientHeightFactor, 0.5)
|
||||
XCTAssertEqual(glideshow.isGradientEnabled, false)
|
||||
|
||||
glideshow.gradientHeightFactor = 0.8
|
||||
|
||||
XCTAssertEqual(glideshow.gradientHeightFactor, 0.8)
|
||||
XCTAssertEqual(glideshow.isGradientEnabled, true)
|
||||
|
||||
}
|
||||
|
||||
func test_pageIndicator() {
|
||||
XCTAssertEqual(glideshow.pageIndicator!.pageIndicatorTintColor, .lightGray)
|
||||
XCTAssertEqual(glideshow.pageIndicator!.currentPageIndicatorTintColor, .darkGray)
|
||||
XCTAssertEqual(glideshow.pageIndicator!.numberOfPages, 0)
|
||||
|
||||
glideshow.items = [
|
||||
GlideItem(caption: "", title: "", description: "", backgroundImage: UIImage.checkmark),
|
||||
GlideItem(caption: "", title: "", description: "", backgroundImage: UIImage.checkmark)
|
||||
]
|
||||
|
||||
XCTAssertEqual(glideshow.pageIndicator!.numberOfPages, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "Glideshow",
|
||||
platforms: [
|
||||
.iOS(.v12)
|
||||
.iOS(.v11)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||

|
||||
|
||||
# Glideshow
|
||||
[](https://shields.io/) [](https://shields.io/) [](https://shields.io/) [](https://shields.io/)
|
||||
[](https://shields.io/) [](https://shields.io/) [](https://shields.io/) [](https://shields.io/)
|
||||
|
||||
A slideshow with *pizzazz!* Glideshow adds transitions to the slideshows labels to set it apart from conventional slideshows
|
||||
A slideshow with *pizzazz!* Glideshow adds transitions to the slideshows labels to set it apart from other conventional "boring" slideshows,
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
@@ -42,6 +41,8 @@ glideshow.items = [
|
||||
GlideItem(description: "General Kenobi!", backgroundImage: UIImage(named: "image2")),
|
||||
GlideItem(title : "Hello there", backgroundImage: UIImage(named: "image3")),
|
||||
GlideItem(title : "Hello there", description: "General Kenobi!")
|
||||
// Network images
|
||||
GlideItem(caption : "Hello there", description: "General Kenobi!", imageURL: "[ IMAGE URL ]")
|
||||
]
|
||||
|
||||
```
|
||||
@@ -67,14 +68,27 @@ The behaviour is configurable by the following properties
|
||||
| `titleColor` | Sets the `textColor` of the slide title |
|
||||
| `descriptionColor` | Sets the `textColor` of the slide description |
|
||||
| `captionColor` | Sets the `textColor` of the slide slide caption |
|
||||
| `titleGlideFactor` | Configures the speed of the transition of the title label ( Default 3 ) |
|
||||
| `titleGlideFactor` | Configures the speed of the transition of the title label ( Default 2 ) |
|
||||
| `descriptionGlideFactor` | Configures the speed of the transition of the description label ( Default 3 ) |
|
||||
| `captionGlideFactor` | Configures the speed of the transition of the caption label ( Default 3 ) |
|
||||
| `captionGlideFactor` | Configures the speed of the transition of the caption label ( Default 1 ) |
|
||||
| `pageIndicatorPosition` | Configures positon of the pge indicator |
|
||||
| `placeHolderImage` | Configures slide background placeholder if a URL is specified, else `nil` |
|
||||
|
||||
### Work in Progress
|
||||
### Contributions
|
||||
|
||||
* Support for Network images
|
||||
* Multiple animations
|
||||
A few issue to tackle
|
||||
- [ ] Pass slide data through delegates
|
||||
- [x] Support for Network images
|
||||
- [ ] Multiple animations
|
||||
- [ ] Customizable label positionings
|
||||
- [ ] Code optimizations
|
||||
|
||||
Steps to contribution:
|
||||
|
||||
1. Fork the repo
|
||||
2. Clone the project
|
||||
3. Make your changes
|
||||
4. Make a pull request
|
||||
|
||||
If you have any question please feel free to open an issue
|
||||
|
||||
|
||||