Files
2026-03-05 16:59:19 +08:00

308 lines
12 KiB
Objective-C

//
// DNSDemoViewController.m
// NewHttpDNSTestDemo
//
// @author Created by Claude Code on 2025-10-05
//
#import "DemoViewController.h"
#import "DemoResolveModel.h"
#import "DemoLogViewController.h"
#import "DemoHttpdnsScenario.h"
@interface DemoViewController () <DemoHttpdnsScenarioDelegate>
@property (nonatomic, strong) DemoHttpdnsScenario *scenario;
@property (nonatomic, strong) DemoHttpdnsScenarioConfig *scenarioConfig;
@property (nonatomic, strong) DemoResolveModel *model;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIStackView *stack;
@property (nonatomic, strong) UITextField *hostField;
@property (nonatomic, strong) UISegmentedControl *ipTypeSeg;
// Removed toggles: HTTPS, Persist, Reuse
@property (nonatomic, strong) UILabel *elapsedLabel;
@property (nonatomic, strong) UILabel *ttlLabel;
@property (nonatomic, strong) UITextView *resultTextView;
@property (nonatomic, weak) DemoLogViewController *presentedLogVC;
@property (nonatomic, strong) UIButton *btnResolve;
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator;
@end
@implementation DemoViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"HTTPDNS Demo";
self.view.backgroundColor = [UIColor systemBackgroundColor];
self.scenario = [[DemoHttpdnsScenario alloc] initWithDelegate:self];
self.model = self.scenario.model;
self.scenarioConfig = [[DemoHttpdnsScenarioConfig alloc] init];
[self buildUI];
[self reloadUIFromModel:self.model];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"日志" style:UIBarButtonItemStylePlain target:self action:@selector(onShowLog)];
}
- (void)buildUI {
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.scrollView];
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]];
self.stack = [[UIStackView alloc] init];
self.stack.axis = UILayoutConstraintAxisVertical;
self.stack.spacing = 12.0;
self.stack.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.stack];
[NSLayoutConstraint activateConstraints:@[
[self.stack.topAnchor constraintEqualToAnchor:self.scrollView.topAnchor constant:16],
[self.stack.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[self.stack.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[self.stack.bottomAnchor constraintEqualToAnchor:self.scrollView.bottomAnchor constant:-16],
[self.stack.widthAnchor constraintEqualToAnchor:self.view.widthAnchor constant:-32]
]];
UIStackView *row1 = [self labeledRow:@"Host"];
self.hostField = [[UITextField alloc] init];
self.hostField.placeholder = @"YOUR_SERVICE_DOMAIN";
self.hostField.text = self.scenarioConfig.host ?: @"";
self.hostField.borderStyle = UITextBorderStyleRoundedRect;
[row1 addArrangedSubview:self.hostField];
[self.stack addArrangedSubview:row1];
UIStackView *row2 = [self labeledRow:@"IP Type"];
self.ipTypeSeg = [[UISegmentedControl alloc] initWithItems:@[@"IPv4", @"IPv6", @"Both"]];
self.ipTypeSeg.selectedSegmentIndex = [self segmentIndexForIpType:self.scenarioConfig.ipType];
[self.ipTypeSeg addTarget:self action:@selector(onIPTypeChanged:) forControlEvents:UIControlEventValueChanged];
[row2 addArrangedSubview:self.ipTypeSeg];
[self.stack addArrangedSubview:row2];
// Removed options stack from UI
UIStackView *actions = [[UIStackView alloc] init];
actions.axis = UILayoutConstraintAxisHorizontal;
actions.spacing = 12;
actions.distribution = UIStackViewDistributionFillEqually;
[self.stack addArrangedSubview:actions];
self.btnResolve = [self filledButton:@"Resolve IP & Test Request" action:@selector(onResolve)];
[actions addArrangedSubview:self.btnResolve];
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
self.activityIndicator.hidesWhenStopped = YES;
[actions addArrangedSubview:self.activityIndicator];
UIStackView *info = [self labeledRow:@"Info"];
self.elapsedLabel = [self monoLabel:@"elapsed: - ms"];
self.ttlLabel = [self monoLabel:@"ttl v4/v6: -/- s"];
[info addArrangedSubview:self.elapsedLabel];
[info addArrangedSubview:self.ttlLabel];
[self.stack addArrangedSubview:info];
if (@available(iOS 11.0, *)) { [self.stack setCustomSpacing:24 afterView:info]; }
UILabel *resultTitle = [[UILabel alloc] init];
resultTitle.text = @"结果";
resultTitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
[self.stack addArrangedSubview:resultTitle];
self.resultTextView = [[UITextView alloc] init];
self.resultTextView.translatesAutoresizingMaskIntoConstraints = NO;
self.resultTextView.editable = NO;
if (@available(iOS 13.0, *)) {
self.resultTextView.font = [UIFont monospacedSystemFontOfSize:12 weight:UIFontWeightRegular];
self.resultTextView.textColor = [UIColor labelColor];
} else {
self.resultTextView.font = [UIFont systemFontOfSize:12];
self.resultTextView.textColor = [UIColor blackColor];
}
self.resultTextView.backgroundColor = [UIColor clearColor];
self.resultTextView.textContainerInset = UIEdgeInsetsMake(8, 12, 8, 12);
[self.stack addArrangedSubview:self.resultTextView];
[self.resultTextView.heightAnchor constraintEqualToConstant:320].active = YES;
}
- (UIStackView *)labeledRow:(NSString *)title {
UIStackView *row = [[UIStackView alloc] init];
row.axis = UILayoutConstraintAxisHorizontal;
row.alignment = UIStackViewAlignmentCenter;
row.spacing = 8.0;
UILabel *label = [[UILabel alloc] init];
label.text = title;
[label.widthAnchor constraintGreaterThanOrEqualToConstant:64].active = YES;
[row addArrangedSubview:label];
return row;
}
- (UILabel *)monoLabel:(NSString *)text {
UILabel *l = [[UILabel alloc] init];
l.text = text;
if (@available(iOS 13.0, *)) {
l.font = [UIFont monospacedSystemFontOfSize:12 weight:UIFontWeightRegular];
} else {
l.font = [UIFont systemFontOfSize:12];
}
return l;
}
- (UIButton *)filledButton:(NSString *)title action:(SEL)sel {
UIButton *b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:title forState:UIControlStateNormal];
b.backgroundColor = [UIColor systemBlueColor];
[b setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
b.layer.cornerRadius = 8;
[b.heightAnchor constraintEqualToConstant:44].active = YES;
[b addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside];
return b;
}
- (UIButton *)borderButton:(NSString *)title action:(SEL)sel {
UIButton *b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:title forState:UIControlStateNormal];
b.layer.borderWidth = 1;
b.layer.borderColor = [UIColor systemBlueColor].CGColor;
b.layer.cornerRadius = 8;
[b.heightAnchor constraintEqualToConstant:44].active = YES;
[b addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside];
return b;
}
- (UIView *)switchItem:(NSString *)title action:(SEL)sel out:(UISwitch * __strong *)outSwitch {
UIStackView *box = [[UIStackView alloc] init];
box.axis = UILayoutConstraintAxisVertical;
box.alignment = UIStackViewAlignmentCenter;
UILabel *l = [[UILabel alloc] init];
l.text = title;
UISwitch *s = [[UISwitch alloc] init];
[s addTarget:self action:sel forControlEvents:UIControlEventValueChanged];
[box addArrangedSubview:s];
[box addArrangedSubview:l];
if (outSwitch != NULL) {
*outSwitch = s;
}
return box;
}
- (NSInteger)segmentIndexForIpType:(HttpdnsQueryIPType)ipType {
switch (ipType) {
case HttpdnsQueryIPTypeIpv4: { return 0; }
case HttpdnsQueryIPTypeIpv6: { return 1; }
default: { return 2; }
}
}
#pragma mark - Actions
- (void)onIPTypeChanged:(UISegmentedControl *)seg {
HttpdnsQueryIPType type = HttpdnsQueryIPTypeBoth;
switch (seg.selectedSegmentIndex) {
case 0: type = HttpdnsQueryIPTypeIpv4; break;
case 1: type = HttpdnsQueryIPTypeIpv6; break;
default: type = HttpdnsQueryIPTypeBoth; break;
}
self.model.ipType = type;
self.scenarioConfig.ipType = type;
[self.scenario applyConfig:self.scenarioConfig];
}
// Toggles delegates removed
- (void)onResolve {
[self.view endEditing:YES];
// Show loading state
self.btnResolve.enabled = NO;
[self.btnResolve setTitle:@"Resolving..." forState:UIControlStateNormal];
[self.activityIndicator startAnimating];
NSString *host = self.hostField.text.length > 0 ? self.hostField.text : @"";
self.model.host = host;
self.scenarioConfig.host = host;
[self.scenario applyConfig:self.scenarioConfig];
[self.scenario resolve];
}
- (void)onShowLog {
DemoLogViewController *logVC = [DemoLogViewController new];
[logVC setInitialText:[self.scenario logSnapshot]];
self.presentedLogVC = logVC;
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:logVC];
nav.modalPresentationStyle = UIModalPresentationAutomatic;
[self presentViewController:nav animated:YES completion:nil];
}
- (void)reloadUIFromModel:(DemoResolveModel *)model {
self.model = model;
// Hide loading state
self.btnResolve.enabled = YES;
[self.btnResolve setTitle:@"Resolve IP & Test Request" forState:UIControlStateNormal];
[self.activityIndicator stopAnimating];
if (![self.hostField isFirstResponder]) {
self.hostField.text = model.host;
}
NSInteger segIndex = [self segmentIndexForIpType:model.ipType];
if (self.ipTypeSeg.selectedSegmentIndex != segIndex) {
self.ipTypeSeg.selectedSegmentIndex = segIndex;
}
self.elapsedLabel.text = [NSString stringWithFormat:@"elapsed: %.0f ms", model.elapsedMs];
self.ttlLabel.text = [NSString stringWithFormat:@"ttl v4/v6: %.0f/%.0f s", model.ttlV4, model.ttlV6];
self.resultTextView.text = [self buildJSONText:model];
}
- (NSString *)buildJSONText:(DemoResolveModel *)model {
NSString *ipTypeStr = @"both";
switch (model.ipType) {
case HttpdnsQueryIPTypeIpv4: { ipTypeStr = @"ipv4"; break; }
case HttpdnsQueryIPTypeIpv6: { ipTypeStr = @"ipv6"; break; }
default: { ipTypeStr = @"both"; break; }
}
NSMutableDictionary *dict = [@{
@"host": model.host ?: @"",
@"ipType": ipTypeStr,
@"elapsedMs": @(model.elapsedMs),
@"ttl": @{ @"v4": @(model.ttlV4), @"v6": @(model.ttlV6) },
@"ipv4": model.ipv4s ?: @[],
@"ipv6": model.ipv6s ?: @[]
} mutableCopy];
if (model.businessRequestResult.length > 0) {
dict[@"businessRequestResult"] = model.businessRequestResult;
}
NSError *err = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&err];
if (data == nil || err != nil) {
return [NSString stringWithFormat:@"{\n \"error\": \"%@\"\n}", err.localizedDescription ?: @"json serialize failed"];
}
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
#pragma mark - DemoHttpdnsScenarioDelegate
- (void)scenario:(DemoHttpdnsScenario *)scenario didUpdateModel:(DemoResolveModel *)model {
[self reloadUIFromModel:model];
}
- (void)scenario:(DemoHttpdnsScenario *)scenario didAppendLogLine:(NSString *)line {
if (self.presentedLogVC != nil) {
[self.presentedLogVC appendLine:line];
}
}
@end