iOS13 获取StatusBar并且获取网络状态

9,087 阅读4分钟

获取StatusBar

项目中通过StatusBar来获取手机当前状态,但是在iOS 13中便获取不到了,调试了一下发现是UIApplication无法获取到statusBar。

    UIApplication *app = [UIApplication sharedApplication];
    id _statusBar = [app valueForKeyPath:@"_statusBar"];

于是改成如下的方式通过UIStatusBarManager获取statusBar。

    UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
    id _statusBar = nil;
    if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
        UIView *_localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
        if ([_localStatusBar respondsToSelector:@selector(statusBar)]) {
            _statusBar = [_localStatusBar performSelector:@selector(statusBar)];
        }
    }

如果只是往StatusBar上添加View,那么到这里就已经可以获取到StatusBar了。

获取网络状态

旧版本中,获取网络状态的代码如下,原理就是获取StatusBar中的网络信号图标,然后通过获取信号图标来获取网络状态。

- (LLNetworkStatus)networkStateFromStatebar {
    __block LLNetworkStatus returnValue = LLNetworkStatusNotReachable;
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            returnValue = [self networkStateFromStatebar];
        });
        return returnValue;
    }

    UIApplication *app = [UIApplication sharedApplication];
    id _statusBar = [app valueForKeyPath:@"_statusBar"];
        
    if ([_statusBar isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")]) {
        // For iPhoneX
        NSArray *children = [[[_statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"foregroundView"] subviews];
        for (UIView *view in children) {
            for (id child in view.subviews) {
                if ([child isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
                    returnValue = LLNetworkStatusReachableViaWiFi;
                    break;
                }
                if ([child isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
                    NSString *originalText = [child valueForKey:@"_originalText"];
                    if ([originalText containsString:@"G"]) {
                        if ([originalText isEqualToString:@"2G"]) {
                            returnValue = LLNetworkStatusReachableViaWWAN2G;
                        } else if ([originalText isEqualToString:@"3G"]) {
                            returnValue = LLNetworkStatusReachableViaWWAN3G;
                        } else if ([originalText isEqualToString:@"4G"]) {
                            returnValue = LLNetworkStatusReachableViaWWAN4G;
                        } else {
                            returnValue = LLNetworkStatusReachableViaWWAN;
                        }
                        break;
                    }
                }
            }
        }
    } else {
        // For others iPhone
        NSArray *children = [[_statusBar valueForKeyPath:@"foregroundView"] subviews];
        int type = -1;
        for (id child in children) {
            if ([child isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
                type = [[child valueForKeyPath:@"dataNetworkType"] intValue];
            }
        }
        switch (type) {
            case 0:
                returnValue = LLNetworkStatusNotReachable;
                break;
            case 1:
                returnValue = LLNetworkStatusReachableViaWWAN2G;
                break;
            case 2:
                returnValue = LLNetworkStatusReachableViaWWAN3G;
                break;
            case 3:
                returnValue = LLNetworkStatusReachableViaWWAN4G;
                break;
            case 4:
                returnValue = LLNetworkStatusReachableViaWWAN;
                break;
            case 5:
                returnValue = LLNetworkStatusReachableViaWiFi;
                break;
            default:
                break;
        }
    }
    return returnValue;
}

虽然在iOS 13中已经可以获取到StatusBar,但是不断递推[StatusBar subviews]时,却不能发现任何一个有关网络信息的View,所以旧的方式并不适用与iOS 13,所以我们打印出StatusBar中所有的属性,查找接下来的思路。

(lldb) po [[[_statusBar valueForKeyPath:@"_statusBar"] class] LL_getPropertyNames]
<__NSArrayM 0x600000192be0>(
items,
displayItemStates,
updateCompletionHandler,
foregroundView,
targetActionable,
accessibilityHUDGestureManager,
visualProviderClassName,
visualProviderClass,
visualProvider,
regions,
dataAggregator,
currentAggregatedData,
containerView,
animationContextId,
animationsEnabled,
styleAttributes,
action,
targetScreen,
style,
foregroundColor,
mode,
orientation,
currentData,
dependentDataEntryKeys,
overlayData,
actionGestureRecognizer,
enabledPartIdentifiers,
avoidanceFrame,
hash,
superclass,
description,
debugDescription
)

在打印的属性中,我们只需要具体分析currentData就可以。(为什么只分析currentData,因为控制导航栏信息的数据都存在currentData中)

(lldb) po [[_statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"]
<_UIStatusBarData: 0x7fdc464362e0: 

mainBatteryEntry=<_UIStatusBarDataBatteryEntry: 0x600000187c30: isEnabled=1, capacity=100, state=2, saverModeActive=0, prominentlyShowsDetailString=0, detailString=100%>, 

secondaryCellularEntry=<_UIStatusBarDataCellularEntry: 0x600002b25440: isEnabled=1, rawValue=0, displayValue=0, displayRawValue=0, status=0, lowDataModeActive=0, type=5, wifiCallingEnabled=0, callForwardingEnabled=0, showsSOSWhenDisabled=0>,

dateEntry=<_UIStatusBarDataStringEntry: 0x600000f17f00: isEnabled=1, stringValue=Tue Aug 27>,

timeEntry=<_UIStatusBarDataStringEntry: 0x600000f17640: isEnabled=1, stringValue=6:34 PM>,

cellularEntry=<_UIStatusBarDataCellularEntry: 0x600002b254a0: isEnabled=1, rawValue=0, displayValue=0, displayRawValue=0, status=1, lowDataModeActive=0, type=5, string=Carrier, wifiCallingEnabled=0, callForwardingEnabled=0, showsSOSWhenDisabled=0>,

wifiEntry=<_UIStatusBarDataWifiEntry: 0x600001aa1c40: isEnabled=1, rawValue=0, displayValue=3, displayRawValue=0, status=5, lowDataModeActive=0, type=0>,

shortTimeEntry=<_UIStatusBarDataStringEntry: 0x600000f16ac0: isEnabled=1, stringValue=6:34>,

// some descriptions.

这里只是展示了一部分log,如果你想查看全部的属性,可以自己调试看看,在这些属性中,我们可以看到这里有关于时间的dateEntrytimeEntry,还有关于网络的cellularEntrywifiEntry,在所有的Entry中都有isEnabled属性,只有当isEnabledtrue时,这个属性才有意义。通过判断wifiEntry是否可用,来确定是否是WiFi,通过判断cellularEntrytype来判断具体是4G/3G,所以获取网络状态的代码如下:

id _statusBar = nil;
    if (@available(iOS 13.0, *)) {
        /*
         We can still get statusBar using the following code, but this is not recommended.
         */
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
        UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
        if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
            UIView *_localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
            if ([_localStatusBar respondsToSelector:@selector(statusBar)]) {
                _statusBar = [_localStatusBar performSelector:@selector(statusBar)];
            }
        }
#pragma clang diagnostic pop
        if (_statusBar) {
            // _UIStatusBarDataCellularEntry
            id currentData = [[_statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
            id _wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
            id _cellularEntry = [currentData valueForKeyPath:@"cellularEntry"];
            if (_wifiEntry && [[_wifiEntry valueForKeyPath:@"isEnabled"] boolValue]) {
                // If wifiEntry is enabled, is WiFi.
                returnValue = LLNetworkStatusReachableViaWiFi;
            } else if (_cellularEntry && [[_cellularEntry valueForKeyPath:@"isEnabled"] boolValue]) {
                NSNumber *type = [_cellularEntry valueForKeyPath:@"type"];
                if (type) {
                    switch (type.integerValue) {
                        case 5:
                            returnValue = LLNetworkStatusReachableViaWWAN4G;
                            break;
                        case 4:
                            returnValue = LLNetworkStatusReachableViaWWAN3G;
                            break;
                            //                        case 1: // Return 1 when 1G.
                            //                            break;
                        case 0:
                            // Return 0 when no sim card.
                            returnValue = LLNetworkStatusNotReachable;
                        default:
                            returnValue = LLNetworkStatusReachableViaWWAN;
                            break;
                    }
                }
            }
        }
    }

总结

完整的代码如下,当然你也可以查看LLDebugTool - LLNetworkHelper.m 来查看具体的代码。

- (LLNetworkStatus)networkStateFromStatebar {
    __block LLNetworkStatus returnValue = LLNetworkStatusNotReachable;
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            returnValue = [self networkStateFromStatebar];
        });
        return returnValue;
    }
    id _statusBar = nil;
    if (@available(iOS 13.0, *)) {
        /*
         We can still get statusBar using the following code, but this is not recommended.
         */
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
        UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
        if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
            UIView *_localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
            if ([_localStatusBar respondsToSelector:@selector(statusBar)]) {
                _statusBar = [_localStatusBar performSelector:@selector(statusBar)];
            }
        }
#pragma clang diagnostic pop
        if (_statusBar) {
            // _UIStatusBarDataCellularEntry
            id currentData = [[_statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
            id _wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
            id _cellularEntry = [currentData valueForKeyPath:@"cellularEntry"];
            if (_wifiEntry && [[_wifiEntry valueForKeyPath:@"isEnabled"] boolValue]) {
                // If wifiEntry is enabled, is WiFi.
                returnValue = LLNetworkStatusReachableViaWiFi;
            } else if (_cellularEntry && [[_cellularEntry valueForKeyPath:@"isEnabled"] boolValue]) {
                NSNumber *type = [_cellularEntry valueForKeyPath:@"type"];
                if (type) {
                    switch (type.integerValue) {
                        case 5:
                            returnValue = LLNetworkStatusReachableViaWWAN4G;
                            break;
                        case 4:
                            returnValue = LLNetworkStatusReachableViaWWAN3G;
                            break;
                            //                        case 1: // Return 1 when 1G.
                            //                            break;
                        case 0:
                            // Return 0 when no sim card.
                            returnValue = LLNetworkStatusNotReachable;
                        default:
                            returnValue = LLNetworkStatusReachableViaWWAN;
                            break;
                    }
                }
            }
        }
    } else {
        UIApplication *app = [UIApplication sharedApplication];
        _statusBar = [app valueForKeyPath:@"_statusBar"];
        
        if ([_statusBar isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")]) {
            // For iPhoneX
            NSArray *children = [[[_statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"foregroundView"] subviews];
            for (UIView *view in children) {
                for (id child in view.subviews) {
                    if ([child isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
                        returnValue = LLNetworkStatusReachableViaWiFi;
                        break;
                    }
                    if ([child isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
                        NSString *originalText = [child valueForKey:@"_originalText"];
                        if ([originalText containsString:@"G"]) {
                            if ([originalText isEqualToString:@"2G"]) {
                                returnValue = LLNetworkStatusReachableViaWWAN2G;
                            } else if ([originalText isEqualToString:@"3G"]) {
                                returnValue = LLNetworkStatusReachableViaWWAN3G;
                            } else if ([originalText isEqualToString:@"4G"]) {
                                returnValue = LLNetworkStatusReachableViaWWAN4G;
                            } else {
                                returnValue = LLNetworkStatusReachableViaWWAN;
                            }
                            break;
                        }
                    }
                }
            }
        } else {
            // For others iPhone
            NSArray *children = [[_statusBar valueForKeyPath:@"foregroundView"] subviews];
            int type = -1;
            for (id child in children) {
                if ([child isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
                    type = [[child valueForKeyPath:@"dataNetworkType"] intValue];
                }
            }
            switch (type) {
                case 0:
                    returnValue = LLNetworkStatusNotReachable;
                    break;
                case 1:
                    returnValue = LLNetworkStatusReachableViaWWAN2G;
                    break;
                case 2:
                    returnValue = LLNetworkStatusReachableViaWWAN3G;
                    break;
                case 3:
                    returnValue = LLNetworkStatusReachableViaWWAN4G;
                    break;
                case 4:
                    returnValue = LLNetworkStatusReachableViaWWAN;
                    break;
                case 5:
                    returnValue = LLNetworkStatusReachableViaWiFi;
                    break;
                default:
                    break;
            }
        }
    }
    
    return returnValue;
}