Skip to content

Commit a181685

Browse files
committed
Support iPhone SE @ iOS 18, support dynamic userInterfaceStyle
display a slim, centered indicator on top of the clock on iPhone SE if running iOS 18 or newer; support dynamic dark/light mode change; fix the isNetworkActivityIndicatorVisible getter property (always false on iOS 18+);
1 parent 4767d37 commit a181685

File tree

5 files changed

+131
-126
lines changed

5 files changed

+131
-126
lines changed

.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.3.2

FTLinearActivityIndicator.podspec

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
Pod::Spec.new do |s|
22
s.name = 'FTLinearActivityIndicator'
3-
s.version = '1.8'
3+
s.version = '2.0'
44
s.summary = 'Add the missing network activity indicator on newer iPhones'
55

66
s.description = <<-DESC
7-
iPhones with a notch such as X, XS, XR, 11 (Pro), 12-13 (mini, Pro) or Dynamic Island
8-
such as 14 Pro don't display the network activity indicator anymore. This pod brings it back
9-
by placing an activity indicator in the upper right of the screen on top of the regular
10-
status bar items. Since a circular indicator wouldn't fit, a rectangular "KITT scanner"
11-
like indicator with a gradient is shown. The indicator UI can be used standalone or as a
12-
"fix" for the iOS network activity indicator (using the existing API).
7+
Since iOS 18, network activity indicator is no longer displayed. iPhones with a notch
8+
or Dynamic Island never displayed it. This pod brings it back by placing an activity
9+
indicator in the upper right of the screen on top of the regular status bar items
10+
(on top of the clock on iPhone SE running iOS 18+).
11+
Since a circular indicator wouldn't fit, a rectangular "KITT scanner" like indicator
12+
with a gradient is shown. The indicator UI can be used standalone or as a "fix" for
13+
the iOS network activity indicator (using the existing API).
1314
DESC
1415

1516
s.homepage = 'https://github.com/futuretap/FTLinearActivityIndicator'

FTLinearActivityIndicator/Classes/UIApplication+LinearNetworkActivityIndicator.swift

Lines changed: 116 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import UIKit
1212
extension UIApplication {
1313
@objc final public class func configureLinearNetworkActivityIndicatorIfNeeded() {
1414
#if !targetEnvironment(macCatalyst) && (swift(<5.9) || !os(visionOS))
15-
if #available(iOS 11.0, *) {
15+
if UIDevice.current.userInterfaceIdiom != .pad {
16+
if #available(iOS 18.0, *) {
17+
configureLinearNetworkActivityIndicator()
18+
}
1619
// detect notch
1720
if let window = shared.windows.first, window.safeAreaInsets.bottom > 0.0 {
18-
if UIDevice.current.userInterfaceIdiom != .pad {
19-
configureLinearNetworkActivityIndicator()
20-
}
21+
configureLinearNetworkActivityIndicator()
2122
}
2223
}
2324
#endif
@@ -26,26 +27,26 @@ extension UIApplication {
2627
#if !targetEnvironment(macCatalyst) && (swift(<5.9) || !os(visionOS))
2728
class func configureLinearNetworkActivityIndicator() {
2829
DispatchQueue.once {
29-
let originalSelector = #selector(setter: UIApplication.isNetworkActivityIndicatorVisible)
30-
let swizzledSelector = #selector(ft_setNetworkActivityIndicatorVisible(visible:))
31-
let originalMethod = class_getInstanceMethod(self, originalSelector)
32-
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
33-
method_exchangeImplementations(originalMethod!, swizzledMethod!)
30+
swizzle(original: #selector(setter: UIApplication.isNetworkActivityIndicatorVisible),
31+
swizzled: #selector(setter: ft_networkActivityIndicatorVisible))
32+
swizzle(original: #selector(getter: UIApplication.isNetworkActivityIndicatorVisible),
33+
swizzled: #selector(getter: ft_networkActivityIndicatorVisible))
3434
}
3535
UIViewController.configureLinearNetworkActivityIndicator()
3636
}
3737

3838
private struct AssociatedKeys {
3939
static var indicatorWindowKey: UInt8 = 0
40+
static var indicatorVisibleKey: UInt8 = 0
4041
}
4142

4243
var indicatorWindow: UIWindow? {
4344
get {
44-
return objc_getAssociatedObject(self, &AssociatedKeys.indicatorWindowKey) as? UIWindow
45+
objc_getAssociatedObject(self, &AssociatedKeys.indicatorWindowKey) as? UIWindow
4546
}
4647

4748
set {
48-
if let newValue = newValue {
49+
if let newValue {
4950
objc_setAssociatedObject(
5051
self,
5152
&AssociatedKeys.indicatorWindowKey,
@@ -57,86 +58,107 @@ extension UIApplication {
5758
}
5859

5960

60-
@objc func ft_setNetworkActivityIndicatorVisible(visible: Bool) {
61-
self.ft_setNetworkActivityIndicatorVisible(visible: visible) // original implementation
62-
63-
if visible {
64-
if indicatorWindow == nil {
65-
let frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 12)
66-
indicatorWindow = UIWindow(frame: frame)
67-
indicatorWindow?.windowLevel = UIWindow.Level.statusBar + 1
68-
indicatorWindow?.isUserInteractionEnabled = false
69-
70-
// notched iPhones differ in corner radius and right notch width
71-
// => lookup margin from right window edge, and width
72-
let layout: [ModelName: (CGFloat, CGFloat)] = [
73-
.iPhoneX1: (74, 44),
74-
.iPhoneX2: (74, 44),
75-
.iPhoneXs: (74, 44),
76-
.iPhoneXsMax1: (74, 44),
77-
.iPhoneXsMax2: (74, 44),
78-
.iPhoneXR: (70, 40),
79-
.iPhone11: (70, 40),
80-
.iPhone11Pro: (60, 34),
81-
.iPhone11ProMax: (74, 44),
82-
.iPhone12Mini: (60, 30),
83-
.iPhone12: (72, 34),
84-
.iPhone12Pro: (72, 34),
85-
.iPhone12ProMax: (80, 42),
86-
.iPhone13Mini: (60, 30),
87-
.iPhone13: (72, 34),
88-
.iPhone13Pro: (72, 34),
89-
.iPhone13ProMax: (80, 42),
90-
.iPhone14: (72, 34),
91-
.iPhone14Plus: (80, 42),
92-
.iPhone14Pro: (72, 34),
93-
.iPhone14ProMax: (80, 42),
94-
.iPhone15: (72, 34),
95-
.iPhone15Plus: (80, 42),
96-
.iPhone15Pro: (72, 34),
97-
.iPhone15ProMax: (80, 42),
98-
.iPhone16: (84, 36),
99-
.iPhone16Plus: (96, 42),
100-
.iPhone16Pro: (86, 38),
101-
.iPhone16ProMax: (96, 42),
102-
]
103-
let modelName = UIDevice.current.ftModelName
104-
let config = modelName.flatMap { layout[$0] } ?? (74, 44)
105-
106-
let x = indicatorWindow!.frame.width - config.0
107-
let width = config.1
108-
109-
let indicator = FTLinearActivityIndicator(frame: CGRect(x: x, y: 7, width: width, height: 4.5))
110-
indicator.isUserInteractionEnabled = false
111-
indicator.hidesWhenStopped = false
112-
indicator.startAnimating()
113-
indicatorWindow?.addSubview(indicator)
114-
}
61+
@objc var ft_networkActivityIndicatorVisible: Bool {
62+
get {
63+
objc_getAssociatedObject(self, &AssociatedKeys.indicatorVisibleKey) as? Bool ?? false
11564
}
116-
guard let indicator = indicatorWindow?.subviews.first as? FTLinearActivityIndicator else {return}
117-
if #available(iOS 13.0, *) {
118-
indicator.tintColor = indicatorWindow?.windowScene?.statusBarManager?.statusBarStyle == .lightContent ? .white : .black
119-
} else {
120-
indicator.tintColor = statusBarStyle == .default ? UIColor.black : UIColor.white
121-
}
122-
if visible {
123-
indicatorWindow?.isHidden = self.isStatusBarHidden
124-
indicator.isHidden = false
125-
indicator.alpha = 1
126-
} else {
127-
UIView.animate(withDuration: 0.5, animations: {
128-
indicator.alpha = 0
129-
}) { (finished) in
130-
if (finished) {
131-
indicator.isHidden = !self.isNetworkActivityIndicatorVisible // might have changed in the meantime
132-
self.indicatorWindow?.isHidden = !self.isNetworkActivityIndicatorVisible || self.isStatusBarHidden
65+
set {
66+
self.ft_networkActivityIndicatorVisible = newValue // original implementation
67+
objc_setAssociatedObject(self, &AssociatedKeys.indicatorVisibleKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
68+
69+
if newValue {
70+
if indicatorWindow == nil {
71+
let frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 12)
72+
indicatorWindow = UIWindow(frame: frame)
73+
indicatorWindow?.windowLevel = UIWindow.Level.statusBar + 1
74+
indicatorWindow?.isUserInteractionEnabled = false
75+
if #available(iOS 17.0, *) {
76+
indicatorWindow?.registerForTraitChanges([UITraitUserInterfaceStyle.self], handler: { (newTC: UITraitEnvironment, previousTraitCollection: UITraitCollection) in
77+
self.ftUpdateNetworkActivityIndicatorAppearance()
78+
})
79+
}
80+
// notched iPhones differ in corner radius and right notch width
81+
// => lookup margin from right window edge, and width
82+
let layout: [ModelName: (CGFloat, CGFloat)] = [
83+
.iPhoneX1: (74, 44),
84+
.iPhoneX2: (74, 44),
85+
.iPhoneXs: (74, 44),
86+
.iPhoneXsMax1: (74, 44),
87+
.iPhoneXsMax2: (74, 44),
88+
.iPhoneXR: (70, 40),
89+
.iPhone11: (70, 40),
90+
.iPhone11Pro: (60, 34),
91+
.iPhone11ProMax: (74, 44),
92+
.iPhone12Mini: (60, 30),
93+
.iPhone12: (72, 34),
94+
.iPhone12Pro: (72, 34),
95+
.iPhone12ProMax: (80, 42),
96+
.iPhone13Mini: (60, 30),
97+
.iPhone13: (72, 34),
98+
.iPhone13Pro: (72, 34),
99+
.iPhone13ProMax: (80, 42),
100+
.iPhone14: (72, 34),
101+
.iPhone14Plus: (80, 42),
102+
.iPhone14Pro: (72, 34),
103+
.iPhone14ProMax: (80, 42),
104+
.iPhone15: (72, 34),
105+
.iPhone15Plus: (80, 42),
106+
.iPhone15Pro: (72, 34),
107+
.iPhone15ProMax: (80, 42),
108+
.iPhone16: (84, 36),
109+
.iPhone16Plus: (96, 42),
110+
.iPhone16Pro: (86, 38),
111+
.iPhone16ProMax: (96, 42),
112+
.iPhoneSE2: (177, 40),
113+
.iPhoneSE3: (177, 40),
114+
]
115+
let modelName = UIDevice.current.ftModelName
116+
let config = modelName.flatMap { layout[$0] } ?? (74, 44)
117+
118+
let x = indicatorWindow!.frame.width - config.0
119+
let width = config.1
120+
121+
let indicator = FTLinearActivityIndicator(frame: CGRect(x: x, y: 7, width: width, height: 4.5))
122+
if [.iPhoneSE2, .iPhoneSE3].contains(modelName) {
123+
indicator.frame = CGRect(x: 172, y: 1.5, width: 31, height: 3)
124+
}
125+
indicator.isUserInteractionEnabled = false
126+
indicator.hidesWhenStopped = false
127+
indicator.startAnimating()
128+
indicatorWindow?.addSubview(indicator)
129+
}
130+
}
131+
guard let indicator = indicatorWindow?.subviews.first as? FTLinearActivityIndicator else {return}
132+
if #available(iOS 13.0, *) {
133+
let style = indicatorWindow?.windowScene?.statusBarManager?.statusBarStyle ?? .default
134+
indicator.tintColor = switch style {
135+
case .lightContent: .white
136+
case .darkContent: .black
137+
default: indicatorWindow?.traitCollection.userInterfaceStyle == .dark ? .white : .black
138+
}
139+
} else {
140+
indicator.tintColor = statusBarStyle == .default ? UIColor.black : UIColor.white
141+
}
142+
if newValue {
143+
indicatorWindow?.isHidden = self.isStatusBarHidden
144+
indicator.isHidden = false
145+
indicator.alpha = 1
146+
} else {
147+
UIView.animate(withDuration: 0.5, animations: {
148+
indicator.alpha = 0
149+
}) { (finished) in
150+
if (finished) {
151+
indicator.isHidden = !self.isNetworkActivityIndicatorVisible // might have changed in the meantime
152+
self.indicatorWindow?.isHidden = !self.isNetworkActivityIndicatorVisible || self.isStatusBarHidden
153+
}
133154
}
134155
}
135156
}
136157
}
137158

138159
func ftUpdateNetworkActivityIndicatorAppearance() {
139160
self.indicatorWindow?.isHidden = !self.isNetworkActivityIndicatorVisible || self.isStatusBarHidden
161+
isNetworkActivityIndicatorVisible = isNetworkActivityIndicatorVisible
140162
}
141163
#endif
142164
}
@@ -146,11 +168,8 @@ extension UIApplication {
146168
extension UIViewController {
147169
@objc final public class func configureLinearNetworkActivityIndicator() {
148170
DispatchQueue.once {
149-
let originalSelector = #selector(setNeedsStatusBarAppearanceUpdate)
150-
let swizzledSelector = #selector(ftSetNeedsStatusBarAppearanceUpdate)
151-
let originalMethod = class_getInstanceMethod(self, originalSelector)
152-
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
153-
method_exchangeImplementations(originalMethod!, swizzledMethod!)
171+
swizzle(original: #selector(setNeedsStatusBarAppearanceUpdate),
172+
swizzled: #selector(ftSetNeedsStatusBarAppearanceUpdate))
154173
}
155174
}
156175

@@ -190,4 +209,13 @@ extension DispatchQueue {
190209
block()
191210
}
192211
}
212+
213+
@available(iOSApplicationExtension, unavailable)
214+
extension NSObject {
215+
fileprivate class func swizzle(original: Selector, swizzled: Selector) {
216+
let originalMethod = class_getInstanceMethod(self as AnyClass, original)
217+
let swizzledMethod = class_getInstanceMethod(self as AnyClass, swizzled)
218+
method_exchangeImplementations(originalMethod!, swizzledMethod!)
219+
}
220+
}
193221
#endif

FTLinearActivityIndicator/Classes/UIDevice+Extension.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public enum ModelName: String {
3737
case iPhone16Plus = "iPhone17,4"
3838
case iPhone16Pro = "iPhone17,1"
3939
case iPhone16ProMax = "iPhone17,2"
40+
case iPhoneSE2 = "iPhone12,8"
41+
case iPhoneSE3 = "iPhone14,6"
4042
}
4143

4244
public extension UIDevice {

README.md

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,10 @@
77
[![Sponsor](https://img.shields.io/badge/Sponsor-ff40a0)](https://github.com/sponsors/futuretap)
88
[![Mastodon](https://img.shields.io/mastodon/follow/000010558?domain=https%3A%2F%2Fmastodon.cloud)](https://mastodon.cloud/@ortwingentz)
99

10-
iPhones with a notch or Dynamic Island don't display the network activity indicator [anymore](http://www.futuretap.com/blog/fix-for-the-missing-network-activity-indicator-on-iphone-x). This framework brings it
11-
back by placing an activity indicator in the upper right of the screen on top of the
12-
regular status bar items on the following devices:
13-
14-
- iPhone X
15-
- iPhone Xs
16-
- iPhone Xs Max
17-
- iPhone Xʀ
18-
- iPhone 11
19-
- iPhone 11 Pro
20-
- iPhone 11 Pro Max
21-
- iPhone 12
22-
- iPhone 12 mini
23-
- iPhone 12 Pro
24-
- iPhone 12 Pro Max
25-
- iPhone 13
26-
- iPhone 13 mini
27-
- iPhone 13 Pro
28-
- iPhone 13 Pro Max
29-
- iPhone 14
30-
- iPhone 14 Plus
31-
- iPhone 14 Pro
32-
- iPhone 14 Pro Max
33-
- iPhone 15
34-
- iPhone 15 Plus
35-
- iPhone 15 Pro
36-
- iPhone 15 Pro Max
37-
- iPhone 16
38-
- iPhone 16 Plus
39-
- iPhone 16 Pro
40-
- iPhone 16 Pro Max
10+
Since iOS 18, network activity indicator is no longer displayed. iPhones with a notch or Dynamic Island
11+
[never displayed it](http://www.futuretap.com/blog/fix-for-the-missing-network-activity-indicator-on-iphone-x).
12+
This framework brings it back by placing an activity indicator in the upper right of the screen on top of the
13+
regular status bar items (on top of the clock on iPhone SE running iOS 18+).
4114

4215
Since a circular indicator wouldn't fit, a rectangular [KITT scanner](https://giphy.com/gifs/80s-nbc-knight-rider-Bo2WsocASVBm0)-like indicator with a gradient is shown. The indicator UI can be used standalone or as a "fix" for the iOS network activity indicator (using the existing API).
4316

0 commit comments

Comments
 (0)