Merge pull request #121 from kkebo/refine-ruleview

feat: refine `RuleView`
This commit is contained in:
Kenta Kubo 2025-09-03 00:35:16 +09:00 committed by GitHub
commit 5c96c63613
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 396 additions and 324 deletions

View file

@ -3,34 +3,11 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
890B80D5251DC3A20046BAA0 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890B80D4251DC3A20046BAA0 /* DetailView.swift */; };
890B80DF251DC6B50046BAA0 /* Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890B80DE251DC6B50046BAA0 /* Presets.swift */; };
893AA853258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA852258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift */; };
893AA858258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA857258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift */; };
893AA85D258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA85C258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift */; };
893AA862258F998C0060B022 /* NEOnDemandRuleInterfaceType+isSSIDUsed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA861258F998C0060B022 /* NEOnDemandRuleInterfaceType+isSSIDUsed.swift */; };
893AA867258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA866258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift */; };
893AA86C258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA86B258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift */; };
893AA871258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893AA870258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift */; };
8940023C24ACBD2700EBE74B /* DNSecureApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940023B24ACBD2700EBE74B /* DNSecureApp.swift */; };
8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940023D24ACBD2700EBE74B /* ContentView.swift */; };
8940024024ACBD2800EBE74B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8940023F24ACBD2800EBE74B /* Assets.xcassets */; };
8940024324ACBD2800EBE74B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8940024224ACBD2800EBE74B /* Preview Assets.xcassets */; };
8940024E24ACBD2800EBE74B /* DNSecureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940024D24ACBD2800EBE74B /* DNSecureTests.swift */; };
8940025924ACBD2800EBE74B /* DNSecureUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8940025824ACBD2800EBE74B /* DNSecureUITests.swift */; };
8940026924ACBE4900EBE74B /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8940026824ACBE4900EBE74B /* NetworkExtension.framework */; };
894958AD2548405E009691D5 /* RuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894958AC2548405E009691D5 /* RuleView.swift */; };
894F33652C46D2F00060F385 /* RestorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894F33642C46D2A20060F385 /* RestorationView.swift */; };
8963FDFB251DF1BC00E3DFE7 /* Bundle+displayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */; };
8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8986CDCE251D9B3400D947CD /* Resolver.swift */; };
8998041628DCDED800C8B421 /* DoTSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8998041528DCDED800C8B421 /* DoTSections.swift */; };
8998041828DCDEEF00C8B421 /* DoHSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8998041728DCDEEF00C8B421 /* DoHSections.swift */; };
89CB922125209DD100B6983C /* HowToActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CB922025209DD100B6983C /* HowToActivateView.swift */; };
89E8B71A29F80164002C2AEF /* LazyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89E8B71929F80164002C2AEF /* LazyTextField.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -51,40 +28,29 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
890B80D4251DC3A20046BAA0 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = "<group>"; };
890B80DE251DC6B50046BAA0 /* Presets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presets.swift; sourceTree = "<group>"; };
8924EDF324C9CDF1004AF871 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
893AA852258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+CaseIterable.swift"; sourceTree = "<group>"; };
893AA857258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+CustomStringConvertible.swift"; sourceTree = "<group>"; };
893AA85C258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+Codable.swift"; sourceTree = "<group>"; };
893AA861258F998C0060B022 /* NEOnDemandRuleInterfaceType+isSSIDUsed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleInterfaceType+isSSIDUsed.swift"; sourceTree = "<group>"; };
893AA866258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleAction+CaseIterable.swift"; sourceTree = "<group>"; };
893AA86B258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleAction+CustomStringConvertible.swift"; sourceTree = "<group>"; };
893AA870258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEOnDemandRuleAction+Codable.swift"; sourceTree = "<group>"; };
8940023824ACBD2700EBE74B /* DNSecure.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DNSecure.app; sourceTree = BUILT_PRODUCTS_DIR; };
8940023B24ACBD2700EBE74B /* DNSecureApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSecureApp.swift; sourceTree = "<group>"; };
8940023D24ACBD2700EBE74B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
8940023F24ACBD2800EBE74B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8940024224ACBD2800EBE74B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
8940024424ACBD2800EBE74B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8940024924ACBD2800EBE74B /* DNSecureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DNSecureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8940024D24ACBD2800EBE74B /* DNSecureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSecureTests.swift; sourceTree = "<group>"; };
8940024F24ACBD2800EBE74B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8940025424ACBD2800EBE74B /* DNSecureUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DNSecureUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8940025824ACBD2800EBE74B /* DNSecureUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSecureUITests.swift; sourceTree = "<group>"; };
8940025A24ACBD2800EBE74B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8940026624ACBE4900EBE74B /* DNSecure.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DNSecure.entitlements; sourceTree = "<group>"; };
8940026824ACBE4900EBE74B /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
894958AC2548405E009691D5 /* RuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleView.swift; sourceTree = "<group>"; };
894F33642C46D2A20060F385 /* RestorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationView.swift; sourceTree = "<group>"; };
8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+displayName.swift"; sourceTree = "<group>"; };
8986CDCE251D9B3400D947CD /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
8998041528DCDED800C8B421 /* DoTSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoTSections.swift; sourceTree = "<group>"; };
8998041728DCDEEF00C8B421 /* DoHSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoHSections.swift; sourceTree = "<group>"; };
89CB922025209DD100B6983C /* HowToActivateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowToActivateView.swift; sourceTree = "<group>"; };
89E8B71929F80164002C2AEF /* LazyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyTextField.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
899AD03F2E66100500449710 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 8940023724ACBD2700EBE74B /* DNSecure */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
899AD0232E66100500449710 /* DNSecure */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (899AD03F2E66100500449710 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = DNSecure; sourceTree = "<group>"; };
899AD0422E66100E00449710 /* DNSecureTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = DNSecureTests; sourceTree = "<group>"; };
899AD0472E66101100449710 /* DNSecureUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = DNSecureUITests; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
8940023524ACBD2700EBE74B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -111,52 +77,13 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
893AA816258F790D0060B022 /* Views */ = {
isa = PBXGroup;
children = (
894F33642C46D2A20060F385 /* RestorationView.swift */,
8940023D24ACBD2700EBE74B /* ContentView.swift */,
89CB922025209DD100B6983C /* HowToActivateView.swift */,
890B80D4251DC3A20046BAA0 /* DetailView.swift */,
894958AC2548405E009691D5 /* RuleView.swift */,
8998041528DCDED800C8B421 /* DoTSections.swift */,
8998041728DCDEEF00C8B421 /* DoHSections.swift */,
89E8B71929F80164002C2AEF /* LazyTextField.swift */,
);
path = Views;
sourceTree = "<group>";
};
893AA817258F79F70060B022 /* Models */ = {
isa = PBXGroup;
children = (
8986CDCE251D9B3400D947CD /* Resolver.swift */,
890B80DE251DC6B50046BAA0 /* Presets.swift */,
);
path = Models;
sourceTree = "<group>";
};
893AA818258F7A0C0060B022 /* Extensions */ = {
isa = PBXGroup;
children = (
8963FDFA251DF1BC00E3DFE7 /* Bundle+displayName.swift */,
893AA852258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift */,
893AA857258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift */,
893AA85C258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift */,
893AA861258F998C0060B022 /* NEOnDemandRuleInterfaceType+isSSIDUsed.swift */,
893AA866258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift */,
893AA86B258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift */,
893AA870258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
8940022F24ACBD2700EBE74B = {
isa = PBXGroup;
children = (
8924EDF324C9CDF1004AF871 /* README.md */,
8940023A24ACBD2700EBE74B /* DNSecure */,
8940024C24ACBD2800EBE74B /* DNSecureTests */,
8940025724ACBD2800EBE74B /* DNSecureUITests */,
899AD0232E66100500449710 /* DNSecure */,
899AD0422E66100E00449710 /* DNSecureTests */,
899AD0472E66101100449710 /* DNSecureUITests */,
8940023924ACBD2700EBE74B /* Products */,
8940026724ACBE4900EBE74B /* Frameworks */,
);
@ -172,47 +99,6 @@
name = Products;
sourceTree = "<group>";
};
8940023A24ACBD2700EBE74B /* DNSecure */ = {
isa = PBXGroup;
children = (
8940026624ACBE4900EBE74B /* DNSecure.entitlements */,
8940023B24ACBD2700EBE74B /* DNSecureApp.swift */,
893AA816258F790D0060B022 /* Views */,
893AA817258F79F70060B022 /* Models */,
893AA818258F7A0C0060B022 /* Extensions */,
8940023F24ACBD2800EBE74B /* Assets.xcassets */,
8940024424ACBD2800EBE74B /* Info.plist */,
8940024124ACBD2800EBE74B /* Preview Content */,
);
path = DNSecure;
sourceTree = "<group>";
};
8940024124ACBD2800EBE74B /* Preview Content */ = {
isa = PBXGroup;
children = (
8940024224ACBD2800EBE74B /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
8940024C24ACBD2800EBE74B /* DNSecureTests */ = {
isa = PBXGroup;
children = (
8940024D24ACBD2800EBE74B /* DNSecureTests.swift */,
8940024F24ACBD2800EBE74B /* Info.plist */,
);
path = DNSecureTests;
sourceTree = "<group>";
};
8940025724ACBD2800EBE74B /* DNSecureUITests */ = {
isa = PBXGroup;
children = (
8940025824ACBD2800EBE74B /* DNSecureUITests.swift */,
8940025A24ACBD2800EBE74B /* Info.plist */,
);
path = DNSecureUITests;
sourceTree = "<group>";
};
8940026724ACBE4900EBE74B /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -236,6 +122,9 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
899AD0232E66100500449710 /* DNSecure */,
);
name = DNSecure;
productName = DNSecure;
productReference = 8940023824ACBD2700EBE74B /* DNSecure.app */;
@ -254,6 +143,9 @@
dependencies = (
8940024B24ACBD2800EBE74B /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
899AD0422E66100E00449710 /* DNSecureTests */,
);
name = DNSecureTests;
productName = DNSecureTests;
productReference = 8940024924ACBD2800EBE74B /* DNSecureTests.xctest */;
@ -272,6 +164,9 @@
dependencies = (
8940025624ACBD2800EBE74B /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
899AD0472E66101100449710 /* DNSecureUITests */,
);
name = DNSecureUITests;
productName = DNSecureUITests;
productReference = 8940025424ACBD2800EBE74B /* DNSecureUITests.xctest */;
@ -325,8 +220,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8940024324ACBD2800EBE74B /* Preview Assets.xcassets in Resources */,
8940024024ACBD2800EBE74B /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -351,25 +244,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8986CDCF251D9B3400D947CD /* Resolver.swift in Sources */,
89CB922125209DD100B6983C /* HowToActivateView.swift in Sources */,
890B80D5251DC3A20046BAA0 /* DetailView.swift in Sources */,
893AA853258F99630060B022 /* NEOnDemandRuleInterfaceType+CaseIterable.swift in Sources */,
893AA871258F99AD0060B022 /* NEOnDemandRuleAction+Codable.swift in Sources */,
893AA85D258F997A0060B022 /* NEOnDemandRuleInterfaceType+Codable.swift in Sources */,
894F33652C46D2F00060F385 /* RestorationView.swift in Sources */,
8940023E24ACBD2700EBE74B /* ContentView.swift in Sources */,
894958AD2548405E009691D5 /* RuleView.swift in Sources */,
8998041828DCDEEF00C8B421 /* DoHSections.swift in Sources */,
89E8B71A29F80164002C2AEF /* LazyTextField.swift in Sources */,
890B80DF251DC6B50046BAA0 /* Presets.swift in Sources */,
893AA858258F996F0060B022 /* NEOnDemandRuleInterfaceType+CustomStringConvertible.swift in Sources */,
8940023C24ACBD2700EBE74B /* DNSecureApp.swift in Sources */,
8998041628DCDED800C8B421 /* DoTSections.swift in Sources */,
893AA862258F998C0060B022 /* NEOnDemandRuleInterfaceType+isSSIDUsed.swift in Sources */,
893AA867258F99990060B022 /* NEOnDemandRuleAction+CaseIterable.swift in Sources */,
893AA86C258F99A10060B022 /* NEOnDemandRuleAction+CustomStringConvertible.swift in Sources */,
8963FDFB251DF1BC00E3DFE7 /* Bundle+displayName.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -377,7 +251,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8940024E24ACBD2800EBE74B /* DNSecureTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -385,7 +258,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8940025924ACBD2800EBE74B /* DNSecureUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -188,7 +188,9 @@ extension ContentView: View {
if self.selection == -1 {
HowToActivateView()
} else if let i = self.selection {
self.detailView(at: i)
NavigationStack {
self.detailView(at: i)
}
} else if !self.isEnabled {
HowToActivateView()
} else {

View file

@ -0,0 +1,46 @@
import NetworkExtension
import SwiftUI
struct DNSSearchDomainMatchView {
@Binding var rule: OnDemandRule
}
extension DNSSearchDomainMatchView: View {
var body: some View {
Form {
Section {
ForEach(0..<self.rule.dnsSearchDomainMatch.count, id: \.self) { i in
LazyTextField(
"Search Domain",
// self.$rule.dnsSearchDomainMatch[i] causes crash on deletion
text: .init(
get: { self.rule.dnsSearchDomainMatch[i] },
set: { self.rule.dnsSearchDomainMatch[i] = $0 }
)
)
}
.onDelete { self.rule.dnsSearchDomainMatch.remove(atOffsets: $0) }
.onMove { self.rule.dnsSearchDomainMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add DNS Search Domain") {
self.rule.dnsSearchDomainMatch.append("")
}
} footer: {
Text(
"If the current default search domain is equal to one of the strings in this array and all of the other conditions in the rule match, then the rule matches."
)
}
}
.navigationTitle("DNS Search Domain Match")
.toolbar {
EditButton()
}
}
}
@available(iOS 17, *)
#Preview {
@Previewable @State var rule = OnDemandRule(name: "Preview Rule")
NavigationStack {
DNSSearchDomainMatchView(rule: $rule)
}
}

View file

@ -0,0 +1,46 @@
import NetworkExtension
import SwiftUI
struct DNSServerAddressMatchView {
@Binding var rule: OnDemandRule
}
extension DNSServerAddressMatchView: View {
var body: some View {
Form {
Section {
ForEach(0..<self.rule.dnsServerAddressMatch.count, id: \.self) { i in
LazyTextField(
"IP Address",
// self.$rule.dnsServerAddressMatch[i] causes crash on deletion
text: .init(
get: { self.rule.dnsServerAddressMatch[i] },
set: { self.rule.dnsServerAddressMatch[i] = $0 }
)
)
}
.onDelete { self.rule.dnsServerAddressMatch.remove(atOffsets: $0) }
.onMove { self.rule.dnsServerAddressMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add DNS Server Address") {
self.rule.dnsServerAddressMatch.append("")
}
} footer: {
Text(
"If each of the current default DNS servers is equal to one of the strings in this array and all of the other conditions in the rule match, then the rule matches."
)
}
}
.navigationTitle("DNS Server Address Match")
.toolbar {
EditButton()
}
}
}
@available(iOS 17, *)
#Preview {
@Previewable @State var rule = OnDemandRule(name: "Preview Rule")
NavigationStack {
DNSServerAddressMatchView(rule: $rule)
}
}

View file

@ -22,53 +22,6 @@ struct DetailView {
@MainActor
extension DetailView: View {
var body: some View {
if #available(iOS 16, *) {
self.modernBody
} else {
self.legacyBody
}
}
@available(iOS 16, *)
private var modernBody: some View {
NavigationStack {
Form {
Section {
Toggle("Use This Server", isOn: self.$isOn)
}
Section("Name") {
LazyTextField("Name", text: self.$server.name)
}
self.serverConfigurationSections
Section {
ForEach(self.server.onDemandRules) { rule in
NavigationLink(rule.name, value: rule.id)
}
.onDelete { self.server.onDemandRules.remove(atOffsets: $0) }
.onMove { self.server.onDemandRules.move(fromOffsets: $0, toOffset: $1) }
Button("Add New Rule") {
self.server.onDemandRules
.append(OnDemandRule(name: "New Rule"))
}
} header: {
EditButton()
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(alignment: .leading) {
Text("On Demand Rules")
}
}
}
.navigationDestination(for: UUID.self) { id in
// When RuleView is opened and tap another server on the sidebar, the previous server's rule comes here.
if self.server.onDemandRules.map(\.id).contains(id) {
RuleView(rule: self.binding(for: id))
}
}
}
.navigationTitle(self.server.name)
}
private var legacyBody: some View {
Form {
Section {
Toggle("Use This Server", isOn: self.$isOn)

View file

@ -0,0 +1,35 @@
import NetworkExtension
import SwiftUI
struct InterfaceTypeMatchView {
@Binding var rule: OnDemandRule
}
extension InterfaceTypeMatchView: View {
var body: some View {
Form {
Section {
Picker("Interface Type", selection: self.$rule.interfaceType) {
ForEach(NEOnDemandRuleInterfaceType.allCases, id: \.self) {
Text($0.description)
}
}
.pickerStyle(.inline)
.labelsHidden()
} footer: {
Text(
"If the current primary network interface is of this type and all of the other conditions in the rule match, then the rule matches."
)
}
}
.navigationTitle("Interface Type Match")
}
}
@available(iOS 17, *)
#Preview {
@Previewable @State var rule = OnDemandRule(name: "Preview Rule")
NavigationStack {
InterfaceTypeMatchView(rule: $rule)
}
}

View file

@ -0,0 +1,36 @@
import NetworkExtension
import SwiftUI
struct ProbeURLView {
@Binding var rule: OnDemandRule
private var probeURL: Binding<String> {
.init(
get: { self.rule.probeURL?.absoluteString ?? "" },
set: { self.rule.probeURL = URL(string: $0) }
)
}
}
extension ProbeURLView: View {
var body: some View {
Form {
Section {
LazyTextField("Probe URL", text: self.probeURL)
} footer: {
Text(
"If a request sent to this URL results in a HTTP 200 OK response and all of the other conditions in the rule match, then the rule matches. If you don't want to use this rule, leave it empty."
)
}
}
.navigationTitle("Probe URL")
}
}
@available(iOS 17, *)
#Preview {
@Previewable @State var rule = OnDemandRule(name: "Preview Rule")
NavigationStack {
ProbeURLView(rule: $rule)
}
}

View file

@ -12,6 +12,7 @@ struct RuleView {
@Binding var rule: OnDemandRule
}
@MainActor
extension RuleView: View {
var body: some View {
Form {
@ -19,6 +20,40 @@ extension RuleView: View {
LazyTextField("Name", text: self.$rule.name)
}
Section("Matching Conditions") {
NavigationLink {
InterfaceTypeMatchView(rule: self.$rule)
} label: {
self.interfaceTypeMatchLabel
}
if self.rule.interfaceType.isSSIDUsed {
NavigationLink {
SSIDMatchView(rule: self.$rule)
} label: {
self.ssidMatchLabel
}
}
NavigationLink {
DNSSearchDomainMatchView(rule: self.$rule)
} label: {
self.dnsSearchDomainMatchLabel
}
NavigationLink {
DNSServerAddressMatchView(rule: self.$rule)
} label: {
self.dnsServerAddressMatchLabel
}
NavigationLink {
ProbeURLView(rule: self.$rule)
} label: {
self.probeURLLabel
}
}
Section {
Picker("Action", selection: self.$rule.action) {
ForEach(NEOnDemandRuleAction.allCases, id: \.self) {
@ -26,129 +61,128 @@ extension RuleView: View {
}
}
}
Section {
Picker("Interface Type", selection: self.$rule.interfaceType) {
ForEach(NEOnDemandRuleInterfaceType.allCases, id: \.self) {
Text($0.description)
}
}
} header: {
Text("Interface Type Match")
} footer: {
Text(
"If the current primary network interface is of this type and all of the other conditions in the rule match, then the rule matches."
)
}
if self.rule.interfaceType.isSSIDUsed {
Section {
ForEach(0..<self.rule.ssidMatch.count, id: \.self) { i in
LazyTextField(
"SSID",
// self.$rule.ssidMatch[i] causes crash on deletion
text: .init(
get: { self.rule.ssidMatch[i] },
set: { self.rule.ssidMatch[i] = $0 }
)
)
}
.onDelete { self.rule.ssidMatch.remove(atOffsets: $0) }
.onMove { self.rule.ssidMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add SSID") {
NEHotspotNetwork.fetchCurrent { network in
self.rule.ssidMatch.append(network?.ssid ?? "")
}
}
} header: {
EditButton()
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(alignment: .leading) {
Text("SSID Match")
}
} footer: {
Text(
"If the Service Set Identifier (SSID) of the current primary connected network matches one of the strings in this array and all of the other conditions in the rule match, then the rule matches."
)
}
}
Section {
ForEach(0..<self.rule.dnsSearchDomainMatch.count, id: \.self) { i in
LazyTextField(
"Search Domain",
// self.$rule.dnsSearchDomainMatch[i] causes crash on deletion
text: .init(
get: { self.rule.dnsSearchDomainMatch[i] },
set: { self.rule.dnsSearchDomainMatch[i] = $0 }
)
)
}
.onDelete { self.rule.dnsSearchDomainMatch.remove(atOffsets: $0) }
.onMove { self.rule.dnsSearchDomainMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add DNS Search Domain") {
self.rule.dnsSearchDomainMatch.append("")
}
} header: {
EditButton()
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(alignment: .leading) {
Text("DNS Search Domain Match")
}
} footer: {
Text(
"If the current default search domain is equal to one of the strings in this array and all of the other conditions in the rule match, then the rule matches."
)
}
Section {
ForEach(0..<self.rule.dnsServerAddressMatch.count, id: \.self) { i in
LazyTextField(
"IP Address",
// self.$rule.dnsServerAddressMatch[i] causes crash on deletion
text: .init(
get: { self.rule.dnsServerAddressMatch[i] },
set: { self.rule.dnsServerAddressMatch[i] = $0 }
)
)
}
.onDelete { self.rule.dnsServerAddressMatch.remove(atOffsets: $0) }
.onMove { self.rule.dnsServerAddressMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add DNS Server Address") {
self.rule.dnsServerAddressMatch.append("")
}
} header: {
EditButton()
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(alignment: .leading) {
Text("DNS Server Address Match")
}
} footer: {
Text(
"If each of the current default DNS servers is equal to one of the strings in this array and all of the other conditions in the rule match, then the rule matches."
)
}
Section {
LazyTextField(
"Probe URL",
text: .init(
get: { self.rule.probeURL?.absoluteString ?? "" },
set: { self.rule.probeURL = URL(string: $0) }
)
)
} header: {
Text("Probe URL Match")
} footer: {
Text(
"If a request sent to this URL results in a HTTP 200 OK response and all of the other conditions in the rule match, then the rule matches. If you don't want to use this rule, leave it empty."
)
}
}
.navigationTitle(self.rule.name)
}
@ViewBuilder private var interfaceTypeMatchLabel: some View {
if #available(iOS 16, *) {
LabeledContent("Interface Type Match", value: self.rule.interfaceType.description)
} else {
HStack {
Text("Interface Type Match")
Spacer()
Text(self.rule.interfaceType.description)
.foregroundStyle(.secondary)
}
}
}
@ViewBuilder private var ssidMatchLabel: some View {
if #available(iOS 16, *) {
LabeledContent("SSID Match") {
if !self.rule.ssidMatch.isEmpty {
Text("\(self.rule.ssidMatch.count)")
} else {
Text("Not Used")
}
}
} else {
HStack {
Text("SSID Match")
Spacer()
Group {
if !self.rule.ssidMatch.isEmpty {
Text("\(self.rule.ssidMatch.count)")
} else {
Text("Not Used")
}
}
.foregroundStyle(.secondary)
}
}
}
@ViewBuilder private var dnsSearchDomainMatchLabel: some View {
if #available(iOS 16, *) {
LabeledContent("DNS Search Domain Match") {
if !self.rule.dnsSearchDomainMatch.isEmpty {
Text("\(self.rule.dnsSearchDomainMatch.count)")
} else {
Text("Not Used")
}
}
} else {
HStack {
Text("DNS Search Domain Match")
Spacer()
Group {
if !self.rule.dnsSearchDomainMatch.isEmpty {
Text("\(self.rule.dnsSearchDomainMatch.count)")
} else {
Text("Not Used")
}
}
.foregroundStyle(.secondary)
}
}
}
@ViewBuilder private var dnsServerAddressMatchLabel: some View {
if #available(iOS 16, *) {
LabeledContent("DNS Server Address Match") {
if !self.rule.dnsServerAddressMatch.isEmpty {
Text("\(self.rule.dnsServerAddressMatch.count)")
} else {
Text("Not Used")
}
}
} else {
HStack {
Text("DNS Server Address Match")
Spacer()
Group {
if !self.rule.dnsServerAddressMatch.isEmpty {
Text("\(self.rule.dnsServerAddressMatch.count)")
} else {
Text("Not Used")
}
}
.foregroundStyle(.secondary)
}
}
}
@ViewBuilder private var probeURLLabel: some View {
if #available(iOS 16, *) {
LabeledContent("Probe URL") {
if let url = self.rule.probeURL?.absoluteString {
Text(url)
} else {
Text("Not Used")
}
}
} else {
HStack {
Text("Probe URL")
Spacer()
Group {
if let url = self.rule.probeURL?.absoluteString {
Text(url)
} else {
Text("Not Used")
}
}
.foregroundStyle(.secondary)
}
}
}
}
@available(iOS 17, *)
#Preview {
RuleView(rule: .constant(OnDemandRule(name: "Preview Rule")))
@Previewable @State var rule = OnDemandRule(name: "Preview Rule")
NavigationStack {
RuleView(rule: $rule)
}
}

View file

@ -0,0 +1,48 @@
import NetworkExtension
import SwiftUI
struct SSIDMatchView {
@Binding var rule: OnDemandRule
}
extension SSIDMatchView: View {
var body: some View {
Form {
Section {
ForEach(0..<self.rule.ssidMatch.count, id: \.self) { i in
LazyTextField(
"SSID",
// self.$rule.ssidMatch[i] causes crash on deletion
text: .init(
get: { self.rule.ssidMatch[i] },
set: { self.rule.ssidMatch[i] = $0 }
)
)
}
.onDelete { self.rule.ssidMatch.remove(atOffsets: $0) }
.onMove { self.rule.ssidMatch.move(fromOffsets: $0, toOffset: $1) }
Button("Add SSID") {
NEHotspotNetwork.fetchCurrent { network in
self.rule.ssidMatch.append(network?.ssid ?? "")
}
}
} footer: {
Text(
"If the Service Set Identifier (SSID) of the current primary connected network matches one of the strings in this array and all of the other conditions in the rule match, then the rule matches."
)
}
}
.navigationTitle("SSID Match")
.toolbar {
EditButton()
}
}
}
@available(iOS 17, *)
#Preview {
@Previewable @State var rule = OnDemandRule(name: "Preview Rule")
NavigationStack {
SSIDMatchView(rule: $rule)
}
}