Skip to content

Commit 597aaed

Browse files
authored
Merge pull request #4441 from wikimedia/talk-page-archives-1
Talk Page Archives - Part 1
2 parents 7705fb8 + bd71afc commit 597aaed

15 files changed

+634
-5
lines changed

WMF Framework/Theme.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,6 @@ public class Colors: NSObject {
696696
}
697697
}
698698

699-
700699
@objc(WMFTheme)
701700
public class Theme: NSObject {
702701

@@ -894,3 +893,12 @@ public protocol Themeable: AnyObject {
894893
@objc(applyTheme:)
895894
func apply(theme: Theme) // this might be better as a var theme: Theme { get set } - common VC superclasses could check for viewIfLoaded and call an update method in the setter. This would elminate the need for the viewIfLoaded logic in every applyTheme:
896895
}
896+
897+
// Use for SwiftUI environment objects
898+
public final class ObservableTheme: ObservableObject {
899+
@Published public var theme: Theme
900+
901+
public init(theme: Theme) {
902+
self.theme = theme
903+
}
904+
}

Wikipedia.xcodeproj/project.pbxproj

Lines changed: 106 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import UIKit
2+
3+
class DemoShiftingThreeLineHeaderView: ShiftingTopView, Themeable {
4+
5+
private(set) var theme: Theme
6+
7+
private lazy var headerView: ThreeLineHeaderView = {
8+
let view = ThreeLineHeaderView()
9+
view.topSmallLine.text = "Test 1"
10+
view.middleLargeLine.text = "Test 2"
11+
view.bottomSmallLine.text = "Test 3"
12+
view.translatesAutoresizingMaskIntoConstraints = false
13+
return view
14+
}()
15+
16+
private var topConstraint: NSLayoutConstraint?
17+
18+
init(shiftOrder: Int, theme: Theme) {
19+
self.theme = theme
20+
super.init(shiftOrder: shiftOrder)
21+
}
22+
23+
required init?(coder aDecoder: NSCoder) {
24+
fatalError("init(coder:) has not been implemented")
25+
}
26+
27+
override func setup() {
28+
super.setup()
29+
30+
addSubview(headerView)
31+
32+
let top = headerView.topAnchor.constraint(equalTo: topAnchor)
33+
let bottom = bottomAnchor.constraint(equalTo: headerView.bottomAnchor)
34+
let leading = headerView.leadingAnchor.constraint(equalTo: leadingAnchor)
35+
let trailing = trailingAnchor.constraint(equalTo: headerView.trailingAnchor)
36+
37+
NSLayoutConstraint.activate([
38+
top,
39+
bottom,
40+
leading,
41+
trailing
42+
])
43+
44+
self.topConstraint = top
45+
clipsToBounds = true
46+
apply(theme: theme)
47+
}
48+
49+
// MARK: Overrides
50+
51+
override var contentHeight: CGFloat {
52+
return headerView.frame.height
53+
}
54+
55+
private var isFullyHidden: Bool {
56+
return -(topConstraint?.constant ?? 0) == contentHeight
57+
}
58+
59+
override func shift(amount: CGFloat) -> ShiftingTopView.AmountShifted {
60+
61+
let limitedShiftAmount = max(0, min(amount, contentHeight))
62+
63+
let percent = limitedShiftAmount / contentHeight
64+
alpha = 1.0 - percent
65+
66+
if (self.topConstraint?.constant ?? 0) != -limitedShiftAmount {
67+
self.topConstraint?.constant = -limitedShiftAmount
68+
}
69+
70+
return limitedShiftAmount
71+
}
72+
73+
// MARK: Themeable
74+
75+
func apply(theme: Theme) {
76+
self.theme = theme
77+
backgroundColor = theme.colors.paperBackground
78+
headerView.apply(theme: theme)
79+
}
80+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import UIKit
2+
3+
class ShiftingNavigationBarView: ShiftingTopView, Themeable {
4+
5+
private let navigationItems: [UINavigationItem]
6+
private weak var popDelegate: UIViewController? // Navigation back actions will be forwarded to this view controller
7+
8+
private var topConstraint: NSLayoutConstraint?
9+
10+
private lazy var bar: UINavigationBar = {
11+
let bar = UINavigationBar()
12+
bar.translatesAutoresizingMaskIntoConstraints = false
13+
bar.delegate = self
14+
return bar
15+
}()
16+
17+
init(shiftOrder: Int, navigationItems: [UINavigationItem], popDelegate: UIViewController) {
18+
self.navigationItems = navigationItems
19+
self.popDelegate = popDelegate
20+
super.init(shiftOrder: shiftOrder)
21+
}
22+
23+
required init?(coder: NSCoder) {
24+
fatalError("init(coder:) has not been implemented")
25+
}
26+
27+
override func setup() {
28+
super.setup()
29+
30+
bar.setItems(navigationItems, animated: false)
31+
addSubview(bar)
32+
33+
let top = bar.topAnchor.constraint(equalTo: topAnchor)
34+
let bottom = bottomAnchor.constraint(equalTo: bar.bottomAnchor)
35+
let leading = bar.leadingAnchor.constraint(equalTo: leadingAnchor)
36+
let trailing = trailingAnchor.constraint(equalTo: bar.trailingAnchor)
37+
38+
NSLayoutConstraint.activate([
39+
top,
40+
bottom,
41+
leading,
42+
trailing
43+
])
44+
45+
self.topConstraint = top
46+
47+
clipsToBounds = true
48+
}
49+
50+
// MARK: Overrides
51+
52+
override var contentHeight: CGFloat {
53+
return bar.frame.height
54+
}
55+
56+
private var isFullyHidden: Bool {
57+
return -(topConstraint?.constant ?? 0) == contentHeight
58+
}
59+
60+
override func shift(amount: CGFloat) -> ShiftingTopView.AmountShifted {
61+
62+
// Only allow navigation bar to move just out of frame
63+
let limitedShiftAmount = max(0, min(amount, contentHeight))
64+
65+
// Shrink and fade
66+
let percent = limitedShiftAmount / contentHeight
67+
let barScaleTransform = CGAffineTransformMakeScale(1.0 - (percent/2), 1.0 - (percent/2))
68+
for subview in self.bar.subviews {
69+
for subview in subview.subviews {
70+
subview.transform = barScaleTransform
71+
}
72+
}
73+
alpha = 1.0 - percent
74+
75+
// Shift Y placement
76+
if (self.topConstraint?.constant ?? 0) != -limitedShiftAmount {
77+
self.topConstraint?.constant = -limitedShiftAmount
78+
}
79+
80+
return limitedShiftAmount
81+
}
82+
83+
// MARK: Themeable
84+
85+
func apply(theme: Theme) {
86+
bar.setBackgroundImage(theme.navigationBarBackgroundImage, for: .default)
87+
bar.titleTextAttributes = theme.navigationBarTitleTextAttributes
88+
bar.isTranslucent = false
89+
bar.barTintColor = theme.colors.chromeBackground
90+
bar.shadowImage = theme.navigationBarShadowImage
91+
bar.tintColor = theme.colors.chromeText
92+
}
93+
}
94+
95+
// MARK: Themeable
96+
97+
extension ShiftingNavigationBarView: UINavigationBarDelegate {
98+
func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
99+
popDelegate?.navigationController?.popViewController(animated: true)
100+
return false
101+
}
102+
103+
// Taken from NavigationBar.swift
104+
func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
105+
// During iOS 14's long press to access back history, this function is called *after* the unneeded navigationItems have been popped off.
106+
// However, with our custom navBar the actual articleVC isn't changed. So we need to find the articleVC for the top navItem, and pop to it.
107+
// This should be in `shouldPop`, but as of iOS 14.0, `shouldPop` isn't called when long pressing a back button. Once this is fixed by Apple,
108+
// we should move this to `shouldPop` to improve animations. (Update: A bug tracker was filed w/ Apple, and this won't be fixed anytime soon.
109+
// Apple: "This is expected behavior. Due to side effects that many clients have in the shouldPop handler, we do not consult it when using the back
110+
// button menu. We instead recommend that you hide the back button when you wish to disallow popping past a particular point in the navigation stack.")
111+
if let topNavigationItem = navigationBar.items?.last,
112+
let navController = popDelegate?.navigationController,
113+
let tappedViewController = navController.viewControllers.first(where: {$0.navigationItem == topNavigationItem}) {
114+
popDelegate?.navigationController?.popToViewController(tappedViewController, animated: true)
115+
}
116+
}
117+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import SwiftUI
2+
3+
struct ShiftingScrollView<Content: View>: View {
4+
5+
@EnvironmentObject var data: ShiftingTopViewsData
6+
7+
let axes: Axis.Set
8+
let showsIndicators: Bool
9+
let content: Content
10+
11+
init(
12+
axes: Axis.Set = .vertical,
13+
showsIndicators: Bool = true,
14+
@ViewBuilder content: () -> Content
15+
) {
16+
self.axes = axes
17+
self.showsIndicators = showsIndicators
18+
self.content = content()
19+
}
20+
21+
var body: some View {
22+
ScrollView(axes, showsIndicators: showsIndicators) {
23+
ZStack(alignment:.topLeading) {
24+
GeometryReader { geometry in
25+
Color.clear.preference(
26+
key: ScrollOffsetPreferenceKey.self,
27+
value: geometry.frame(in: .named("scrollView")).origin
28+
)
29+
}
30+
.frame(width: 0, height: 0)
31+
content
32+
.padding(.top, data.totalHeight)
33+
}
34+
}
35+
.coordinateSpace(name: "scrollView")
36+
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
37+
}
38+
39+
func offsetChanged(_ offset: CGPoint) {
40+
data.scrollAmount = -offset.y
41+
}
42+
}
43+
44+
private struct ScrollOffsetPreferenceKey: PreferenceKey {
45+
static var defaultValue: CGPoint = .zero
46+
47+
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
48+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import CocoaLumberjackSwift
3+
4+
class ShiftingTopView: SetupView {
5+
typealias AmountShifted = CGFloat
6+
7+
weak var stackView: ShiftingTopViewsStack?
8+
let shiftOrder: Int
9+
10+
init(shiftOrder: Int) {
11+
self.shiftOrder = shiftOrder
12+
super.init(frame: .zero)
13+
}
14+
15+
required init?(coder: NSCoder) {
16+
fatalError("init(coder:) has not been implemented")
17+
}
18+
19+
override func setup() {
20+
super.setup()
21+
22+
translatesAutoresizingMaskIntoConstraints = false
23+
}
24+
25+
override func layoutSubviews() {
26+
super.layoutSubviews()
27+
28+
if stackView == nil {
29+
DDLogError("Missing stackView assignment in ShiftingSubview, which could potentially cause incorrect content inset/padding calculations.")
30+
}
31+
stackView?.calculateTotalHeight()
32+
}
33+
34+
var contentHeight: CGFloat {
35+
assertionFailure("Must override")
36+
return 0
37+
}
38+
39+
func shift(amount: CGFloat) -> AmountShifted {
40+
assertionFailure("Must override")
41+
return 0
42+
}
43+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
protocol ShiftingTopViewsContaining: UIViewController {
5+
var shiftingTopViewsStack: ShiftingTopViewsStack? { get set }
6+
}
7+
8+
// Note: This subclass only seems necessary for proper nav bar hiding in iOS 14 & 15. It can be removed and switched to raw UIHostingControllers for iOS16+
9+
private class NavigationBarHidingHostingVC<Content: View>: UIHostingController<Content> {
10+
11+
override func viewWillAppear(_ animated: Bool) {
12+
super.viewWillAppear(animated)
13+
14+
self.navigationController?.isNavigationBarHidden = true
15+
}
16+
}
17+
18+
extension ShiftingTopViewsContaining {
19+
func setup(shiftingTopViews: [ShiftingTopView], swiftuiView: some View, observableTheme: ObservableTheme) {
20+
21+
navigationController?.isNavigationBarHidden = true
22+
23+
let shiftingTopViewsStack = ShiftingTopViewsStack()
24+
25+
// Add needed environment objects to SwiftUI view, then embed hosting view controller, then embed SwiftUI hosting view controller
26+
let finalSwiftUIView = swiftuiView
27+
.environmentObject(shiftingTopViewsStack.data)
28+
.environmentObject(observableTheme)
29+
30+
let childHostingVC: UIViewController
31+
32+
if #available(iOS 16, *) {
33+
childHostingVC = UIHostingController(rootView: finalSwiftUIView)
34+
} else {
35+
childHostingVC = NavigationBarHidingHostingVC(rootView: finalSwiftUIView)
36+
}
37+
38+
childHostingVC.view.translatesAutoresizingMaskIntoConstraints = false
39+
view.addSubview(childHostingVC.view)
40+
41+
NSLayoutConstraint.activate([
42+
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: childHostingVC.view.topAnchor),
43+
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: childHostingVC.view.leadingAnchor),
44+
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: childHostingVC.view.trailingAnchor),
45+
view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: childHostingVC.view.bottomAnchor)
46+
])
47+
48+
addChild(childHostingVC)
49+
childHostingVC.didMove(toParent: self)
50+
childHostingVC.view.backgroundColor = .clear
51+
52+
// Add shiftingTopViewsStack
53+
view.addSubview(shiftingTopViewsStack)
54+
55+
NSLayoutConstraint.activate([
56+
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: shiftingTopViewsStack.topAnchor),
57+
view.leadingAnchor.constraint(equalTo: shiftingTopViewsStack.leadingAnchor),
58+
view.trailingAnchor.constraint(equalTo: shiftingTopViewsStack.trailingAnchor)
59+
])
60+
61+
shiftingTopViewsStack.addShiftingTopViews(shiftingTopViews)
62+
shiftingTopViewsStack.apply(theme: observableTheme.theme)
63+
self.shiftingTopViewsStack = shiftingTopViewsStack
64+
}
65+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Foundation
2+
import Combine
3+
4+
class ShiftingTopViewsData: ObservableObject {
5+
@Published var scrollAmount = CGFloat(0)
6+
@Published var totalHeight = CGFloat(0)
7+
}

0 commit comments

Comments
 (0)