Showcase is a UIView based view to display any views in the frame, like you display figures in a shelf or showcase.
- Protocol based - Type-safe for view and model, does not require overriding any special view.
- Looping - Loop first cell to last cell or last cell to first cell being reused.
- Paging - Can enable showcase cell size paging.
- Layout - Customize your own layout.
Add the following to your Podfile:
use_frameworks!
target 'TargetName' do
pod 'Showcase'
end
Add the following to your Package.swift:
github "h-yuya/Showcase"
Configure UIView in the storyboard and set custom class to Showcase
First, make a view you want to display and model are needed for configuration of the view conform to ReusableView
and ReusableModel
protocol.
import UIKit
import Showcase
struct ColorModel: ReusableModel {
let color: UIColor
init(color: UIColor) {
self.color = color
}
}
final class ColorView: ReusableView {
typealias Model = ColorModel
func configure(with model: ColorModel) {
backgroundColor = model.color
}
override func awakeFromNib() {
super.awakeFromNib()
configureAppearance()
}
}
private extension ColorView {
func configureAppearance() {
layer.cornerRadius = 30
clipsToBounds = true
}
}
-
typealias Model
Model
is a model to configure the viewtypealias
it to any model conforming toReusableModel
.
-
func configure(with model: Model)
- As its name suggests, configure the view with
Model
- As its name suggests, configure the view with
import UIKit
import Showcase
final class ColorViewController: UIViewController {
// UIView is the super class of Showcase
@IBOutlet fileprivate weak var showcase: Showcase!
fileprivate let colors: [UIColor] = [.red, .green, .blue, .yellow, .gray]
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
}
private extension ColorViewController {
func configure() {
let models = colors.map(ColorModel.init(color:))
showcase.layout.direction = .horizontal
showcase.layout.itemSize = .init(width: 300, height: 300)
// registered view is able to be used for resetting showcase.
showcase.register(byNibName: ColorView.self)
// Type-safely reset the showcase. You can call this method any number of times.
// views used for resetting must be registered before resetting.
showcase.reset(ColorView.self, models: models)
// The type is used as the identifier for reuse
}
}
Do you want to do paging with each view sizes??
OK, add just showcase.isPagingEnabled = true
before resetting.
Showcase have a layout property. You can design your layout easily. These are all properties layout has.
direction
: LayoutDirection => .horizontal or .verticalitemSize
: CGSize => item sizelineSpacing
: CGFloat => spacing between each cellpath
: PathProtocol => path, view's orbit drawn by scrollingtransform
: TransformProtocol? => view's shape is transformed by scrolling
The Speciality is path
and transform
property.
public protocol PathProtocol {
func normalizedPath(withCoordinate value: CGFloat) -> CGFloat
}
public protocol TransformProtocol {
func transform(withScrollingRate rate: CGFloat) -> CATransform3D
}
- Normalized coordinate
- Can create your own designed path, conforming to PathProtocol
Above, the blue-green area shows the frame of showcase. path
, transform
is normalized as
- The upper boundary of showcase = -1
- The lower boundary of showcase = 1
- The left boundary of showcase = -1
- The right boundary of showcase = 1
x-axis, y-axis are same direction as UIKit's coordinates' direction.
Showcase provides 4 types of path.
DefaultPath
- moves on the x-axis, or y axisCircularPath
- moves on the circular orbitWavePath
- moves wavyLinearPath
- moves linearly
and, 3 types of transform
TransformRotate
- rotation transformTransformScale
- scale transformTransformScaleRotate
- rotation and scale transform
This DefaultPath
is the default path of showcase.
public final class DefaultPath: PathProtocol {
public func normalizedPath(withCoordinate value: CGFloat) -> CGFloat {
return 0
}
public init() {}
}
Here, x
is the x, y-coordinate when the showcase.layout.direction == .horizontal, .vertical.
The normalizedPath
return 0 for all inputs. This indicates views are move on the axis, because 0 is on the axis in the normalized coordinate.
CircularPath
can be used for circular orbit.
public final class CircularPath: PathProtocol {
public var normalizedRadius: CGFloat = 1
public var normalizedOffset: CGFloat = -1
public var offsetAngle: CGFloat = 0
public var angle: CGFloat = .pi / 2
public func normalizedPath(withCoordinate value: CGFloat) -> CGFloat {
let theta = value * angle
return normalizedRadius * cos(theta - offsetAngle) + normalizedOffset
}
public init() {}
}
public class TransformRotate: TransformProtocol {
public var startRate: CGFloat = 0.2
public var rotateAngle: CGFloat = 2 * .pi
public func transform(withScrollingRate rate: CGFloat) -> CATransform3D {
let canRotate = abs(rate) > startRate
let optimizedRate = (rate + (rate < 0 ? startRate : -startRate))
return CATransform3DMakeRotation(canRotate > rotateAngle * optimizedRate : 0, 0, 0, 1)
}
public init() {}
}
Here, the range [-1, 1] is converted to [-.pi / 2, .pi / 2]. After that, the cos() value of the range shows the circular coordinate.
When you use this path, create this path and replace layout.path
with the value in the configuration function.
For TransformProtocol, [-1, 1] is the range of rate of scrolling. Apply it to the parameter you want to change with scrolling, here rotation angle is it.
Replace layout.transform
on using.
Before showing example, I'll change the above configuration.
...
//fileprivate let colors: [UIColor] = [.red, .green, .blue, .yellow, .gray]
fileprivate let colors: [UIColor] = [.red, .green, .blue, .yellow, .gray, .purple, .orange, .cyan]
...
func configure() {
let models = colors.map(ColorModel.init(color:))
let path = CircularPath()
let transform = TransformRotate()
transform.startRate = 0.4
transform.rotateAngle = 8 * .pi
showcase.layout.direction = .vertical
showcase.itemSize = .init(width: 100, height: 100)
showcase.layout.path = path
//showcase.layout.transform = transform
showcase.register(byNibName: ShowView.self)
showcase.reset(ShowView.self, models: models)
}
WavePath
can be used for wavy orbit.
public final class WavePath: PathProtocol {
public var normalizedAmplitude: CGFloat = 0.5
public var normalizedOffset: CGFloat = 0
public var offsetAngle: CGFloat = 0
public var angle: CGFloat = .pi / 2
public func normalizedPath(withCoordinate value: CGFloat) -> CGFloat {
let theta = value * angle
return normalizedAmplitude * sin(theta - offsetAngle) + normalizedOffset
}
public init() {}
}
public class TransformScale: TransformProtocol {
public var startRate: CGFloat = 0.8
public var normalizedMinimumScale: CGFloat = 0.5
public func transform(withScrollingRate rate: CGFloat) -> CATransform3D {
let transformedRate = transformRange(withType: .centerPeak, rate: rate)
let canScale = transformedRate < startRate
let optimizedRate = max(transformedRate + (1 - startRate), normalizedMinimumScale)
return CATransform3DMakeScale(canScale ? optimizedRate : 1, canScale ? optimizedRate : 1, 0)
}
public init() {}
}
The range[-1, 1] is converted to [-.pi / 2, .pi / 2], like CircularPath
. Applying sin() function to input x, the orbit moves wavy.
Just change the above path and transform, like
let path = WavePath()
path.normalizedAmplitude = 0.5
path.angle = .pi
let transform = TransformScale()
transform.normalizedMinimumScale = 0.2
showcase.layout.path = path
showcase.layout.transform = transform
LinearPath
can be used for linear orbit.
public final class LinearPath: PathProtocol {
public var normalizedGradient: CGFloat = -0.5
public var normalizedInversePosition: CGFloat?
public func normalizedPath(withCoordinate value: CGFloat) -> CGFloat {
switch normalizedInversePosition {
case .some(let inverse):
return value < inverse ? -normalizedGradient * value : normalizedGradient * (value - 2 * inverse)
case .none:
return normalizedGradient * value
}
}
public init() {}
}
public class TransformScaleRotate: TransformProtocol {
public var transformRotate: TransformRotate = .init()
public var transformScale: TransformScale = .init()
public func transform(withScrollingRate rate: CGFloat) -> CATransform3D {
return CATransform3DConcat(transformRotate.transform(withScrollingRate: rate), transformScale.transform(withScrollingRate: rate))
}
public init() {}
}
LinearPath
moves views linearly. Here, if you set normalizedInversePosition
, the move will be inversed. So the movement will be like V
.
Just change the above path and transform, like
let path = LinearPath()
path.normalizedInversePosition = 0.3
path.normalizedGradient = 0.8
let rotate = TransformRotate()
rotate.startRate = 0.4
rotate.rotateAngle = 8 * .pi
let scale = TransformScale()
scale.normalizedMinimumScale = 0.2
let transform = TransformScaleRotate()
transform.transformRotate = rotate
transform.transformScale = scale
showcase.layout.path = path
showcase.layout.transform = transform
Like here, transforms comformed to TransformProtocol
can be concated with same scrolling rate.
- Use
ReusableView
andReusableModel
to display views inshowcase
- Do you want to do paging with each cell size? Just use
showcase.isPagingEnabled = true
- Do you want to move views like you imagine? Showcase provides 4 different type of
path
. - If you create your own path, use
PathProtocol
and design it. - And
TransformProtocol
make view transformed. - Showcase is much customizable for
direction
,itemSize
,lineSpacing
,isPagingEnabled
,path
,transform
Showcase is released under the MIT License.