Something went wrong. Try again.
A slopfork of Sam Henri Gold's Twitter client, reskinned like Tweetie 2 / Twitter for iPhone circa 2012. Personal use.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195// Hosts a leading (non-top) column's view. The view controller stays a child of the// navigation controller; only its view lives here until it becomes the top again.// The column's header is managed by the navigation controller and sits above the view.//
import UIKit
final class ColumnCardView: UIView { private(set) weak var viewController: UIViewController? /// Extra rounding while the column is lifted by a swipe-down or shown in the overview. var cornerRadius: CGFloat = 0 { didSet { if cornerRadius != oldValue { setNeedsLayout() } } } /// Darkens a column as newer columns cover it (Tweetie's leftColumnDimmer). let dimmer = UIView() /// How much shadow the card casts (0…1): only as much as its leading edge actually shows, so a pile of cards /// doesn't stack shadow on shadow. var shadowStrength: CGFloat = 1 { didSet { if shadowStrength != oldValue { setNeedsLayout() } } } /// Clips the column and its dimmer to the card's corners, together (so their rounded edges can't fringe /// against each other), leaving the card itself unclipped to cast its shadow. private let content = UIView()
init(viewController: UIViewController) { self.viewController = viewController super.init(frame: .zero) addSubview(content) dimmer.backgroundColor = UIColor(resource: .columnDimmer) dimmer.alpha = 0 dimmer.isUserInteractionEnabled = false content.addSubview(dimmer) }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
/// The card's shape: as cards, the top corners take the card radius and the bottom ones stay square, running /// off the bottom edge; `radius` (lifted or browsed) rounds every corner at least that much. private static func cornerRadii(_ radius: CGFloat) -> (top: CGFloat, bottom: CGFloat) { let settings = ColumnSettings.shared return settings.cardStyle ? (max(radius, settings.cardCornerRadius), radius) : (radius, radius) }
/// Rounds and clips a column (a card's content, or the top column's view) to the card shape. static func applyCorners(_ radius: CGFloat, to view: UIView) { let radii = cornerRadii(radius) view.cornerConfiguration = .uniformTopRadius( .fixed(radii.top), bottomLeftRadius: .fixed(radii.bottom), bottomRightRadius: .fixed(radii.bottom)) view.clipsToBounds = ColumnSettings.shared.cardStyle || radius > 0 }
/// Square, unclipped: a column's view inside its card (the card's content view clips it). static func clearCorners(of view: UIView) { view.cornerConfiguration = .corners(radius: .fixed(0)) view.clipsToBounds = false }
/// Gives `view` a card's drop shadow, shaped like a card occupying `rect` in its coordinates (the view itself /// may be bigger, like a wrapper the column moves inside). Cards only; edge to edge, Tweetie's strips are used. static func applyShadow(to view: UIView, around rect: CGRect, radius: CGFloat = 0, strength: CGFloat = 1) { guard ColumnSettings.shared.cardStyle, !rect.isEmpty else { view.layer.shadowOpacity = 0 return } let layer = view.layer layer.shadowOpacity = Float(0.55 * min(max(strength, 0), 1)) // The path only changes with the card's shape (not every time it moves or its shadow fades). let shape = [rect.minX, rect.minY, rect.width, rect.height, radius, ColumnSettings.shared.cardCornerRadius] guard layer.value(forKey: Self.shadowShapeKey) as? [CGFloat] != shape else { return } layer.setValue(shape, forKey: Self.shadowShapeKey) let radii = cornerRadii(radius) let path = UIBezierPath() path.move(to: CGPoint(x: rect.minX, y: rect.maxY - radii.bottom)) path.addArc( withCenter: CGPoint(x: rect.minX + radii.top, y: rect.minY + radii.top), radius: radii.top, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true) path.addArc( withCenter: CGPoint(x: rect.maxX - radii.top, y: rect.minY + radii.top), radius: radii.top, startAngle: .pi * 1.5, endAngle: 0, clockwise: true) path.addArc( withCenter: CGPoint(x: rect.maxX - radii.bottom, y: rect.maxY - radii.bottom), radius: radii.bottom, startAngle: 0, endAngle: .pi / 2, clockwise: true) path.addArc( withCenter: CGPoint(x: rect.minX + radii.bottom, y: rect.maxY - radii.bottom), radius: radii.bottom, startAngle: .pi / 2, endAngle: .pi, clockwise: true) path.close() layer.shadowColor = UIColor(resource: .columnShadow).cgColor layer.shadowRadius = 14 layer.shadowOffset = .zero layer.shadowPath = path.cgPath }
private static let shadowShapeKey = "columnShadowShape"
/// Takes the view controller's view (no-op if already hosted). func adoptView() { guard let view = viewController?.view, view.superview !== content else { return } view.transform = .identity view.layer.shadowOpacity = 0 Self.clearCorners(of: view) content.insertSubview(view, at: 0) view.frame = content.bounds }
var hostsView: Bool { viewController?.viewIfLoaded?.superview === content }
override func layoutSubviews() { super.layoutSubviews() if content.frame != bounds { content.frame = bounds } if hostsView, let view = viewController?.view, view.frame != bounds { view.frame = bounds } Self.applyCorners(cornerRadius, to: content) Self.applyShadow(to: self, around: bounds, radius: cornerRadius, strength: shadowStrength) dimmer.frame = bounds content.bringSubviewToFront(dimmer) }}
/// Tweetie's column edge shadows: shadow-left.png (23pt, alpha 0.5) and shadow-left-fat.png (227pt, alpha 0.7),/// hanging off a column's leading edge onto whatever it covers.final class ColumnEdgeShadow: UIView { static let reach: CGFloat = 227
private let fine = UIImageView(image: UIImage(named: "shadow-left", in: .module, with: nil)) private let fat = UIImageView(image: UIImage(named: "shadow-left-fat", in: .module, with: nil))
override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = false fat.alpha = 0.7 fine.alpha = 0.5 addSubview(fat) addSubview(fine) }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func layoutSubviews() { super.layoutSubviews() // The images are 1pt tall strips; stretch them down the column. fat.frame = CGRect(x: bounds.maxX - 227, y: 0, width: 227, height: bounds.height) fine.frame = CGRect(x: bounds.maxX - 23, y: 0, width: 23, height: bounds.height) }}
/// Tweetie's throwaway feedback while pulling past the root: two outlined "pages"; the top one tilts away/// once letting go would clear everything back to the root.final class ColumnThrowawayIndicator: UIView { private let back = CALayer() private let front = CALayer() private(set) var isArmed = false
override init(frame: CGRect) { super.init(frame: frame) isUserInteractionEnabled = false alpha = 0 for page in [back, front] { page.borderColor = UIColor(resource: .throwawayPageBorder).cgColor page.borderWidth = 2 page.backgroundColor = UIColor(resource: .throwawayPageFill).cgColor page.cornerRadius = 4 page.cornerCurve = .continuous layer.addSublayer(page) } }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override var intrinsicContentSize: CGSize { CGSize(width: 64, height: 80) }
func setArmed(_ armed: Bool, animated: Bool) { guard armed != isArmed else { return } isArmed = armed CATransaction.begin() CATransaction.setDisableActions(!animated) CATransaction.setAnimationDuration(0.2) front.setAffineTransform(armed ? CGAffineTransform(translationX: 10, y: -8).rotated(by: 0.2) : .identity) front.opacity = armed ? 0.5 : 1 CATransaction.commit() }
override func layoutSubviews() { super.layoutSubviews() let page = CGRect(x: 0, y: 0, width: 46, height: 60) CATransaction.begin() CATransaction.setDisableActions(true) back.bounds = page front.bounds = page back.position = CGPoint(x: bounds.midX - 4, y: bounds.midY + 4) front.position = CGPoint(x: bounds.midX + 4, y: bounds.midY - 4) CATransaction.commit() }}