Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2220f4be52 | |||
| 4e3146f9fe |
@@ -7,19 +7,32 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CF43E868300A7CFA00CB9AE3 /* LearnEnglish.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LearnEnglish.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CF999C8C300BBDE3004D66A6 /* LearnEnglish.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LearnEnglish.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
CF999C9E300BBDE4004D66A6 /* Exceptions for "LearnEnglish" folder in "LearnEnglish" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = CF999C8B300BBDE3004D66A6 /* LearnEnglish */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CF43E86A300A7CFA00CB9AE3 /* LearnEnglish */ = {
|
||||
CF999C8E300BBDE3004D66A6 /* LearnEnglish */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
CF999C9E300BBDE4004D66A6 /* Exceptions for "LearnEnglish" folder in "LearnEnglish" target */,
|
||||
);
|
||||
path = LearnEnglish;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CF43E865300A7CFA00CB9AE3 /* Frameworks */ = {
|
||||
CF999C89300BBDE3004D66A6 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -29,18 +42,18 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CF43E85F300A7CFA00CB9AE3 = {
|
||||
CF999C83300BBDE3004D66A6 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CF43E86A300A7CFA00CB9AE3 /* LearnEnglish */,
|
||||
CF43E869300A7CFA00CB9AE3 /* Products */,
|
||||
CF999C8E300BBDE3004D66A6 /* LearnEnglish */,
|
||||
CF999C8D300BBDE3004D66A6 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CF43E869300A7CFA00CB9AE3 /* Products */ = {
|
||||
CF999C8D300BBDE3004D66A6 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CF43E868300A7CFA00CB9AE3 /* LearnEnglish.app */,
|
||||
CF999C8C300BBDE3004D66A6 /* LearnEnglish.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -48,64 +61,64 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CF43E867300A7CFA00CB9AE3 /* LearnEnglish */ = {
|
||||
CF999C8B300BBDE3004D66A6 /* LearnEnglish */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CF43E873300A7CFB00CB9AE3 /* Build configuration list for PBXNativeTarget "LearnEnglish" */;
|
||||
buildConfigurationList = CF999C9F300BBDE4004D66A6 /* Build configuration list for PBXNativeTarget "LearnEnglish" */;
|
||||
buildPhases = (
|
||||
CF43E864300A7CFA00CB9AE3 /* Sources */,
|
||||
CF43E865300A7CFA00CB9AE3 /* Frameworks */,
|
||||
CF43E866300A7CFA00CB9AE3 /* Resources */,
|
||||
CF999C88300BBDE3004D66A6 /* Sources */,
|
||||
CF999C89300BBDE3004D66A6 /* Frameworks */,
|
||||
CF999C8A300BBDE3004D66A6 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
CF43E86A300A7CFA00CB9AE3 /* LearnEnglish */,
|
||||
CF999C8E300BBDE3004D66A6 /* LearnEnglish */,
|
||||
);
|
||||
name = LearnEnglish;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = LearnEnglish;
|
||||
productReference = CF43E868300A7CFA00CB9AE3 /* LearnEnglish.app */;
|
||||
productReference = CF999C8C300BBDE3004D66A6 /* LearnEnglish.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CF43E860300A7CFA00CB9AE3 /* Project object */ = {
|
||||
CF999C84300BBDE3004D66A6 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2630;
|
||||
LastUpgradeCheck = 2630;
|
||||
TargetAttributes = {
|
||||
CF43E867300A7CFA00CB9AE3 = {
|
||||
CF999C8B300BBDE3004D66A6 = {
|
||||
CreatedOnToolsVersion = 26.3;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CF43E863300A7CFA00CB9AE3 /* Build configuration list for PBXProject "LearnEnglish" */;
|
||||
buildConfigurationList = CF999C87300BBDE3004D66A6 /* Build configuration list for PBXProject "LearnEnglish" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CF43E85F300A7CFA00CB9AE3;
|
||||
mainGroup = CF999C83300BBDE3004D66A6;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = CF43E869300A7CFA00CB9AE3 /* Products */;
|
||||
productRefGroup = CF999C8D300BBDE3004D66A6 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CF43E867300A7CFA00CB9AE3 /* LearnEnglish */,
|
||||
CF999C8B300BBDE3004D66A6 /* LearnEnglish */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CF43E866300A7CFA00CB9AE3 /* Resources */ = {
|
||||
CF999C8A300BBDE3004D66A6 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -115,7 +128,7 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CF43E864300A7CFA00CB9AE3 /* Sources */ = {
|
||||
CF999C88300BBDE3004D66A6 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -125,7 +138,71 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CF43E871300A7CFB00CB9AE3 /* Debug */ = {
|
||||
CF999CA0300BBDE4004D66A6 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 37HGUEC495;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = LearnEnglish/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.fishestlife.english;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CF999CA1300BBDE4004D66A6 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 37HGUEC495;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = LearnEnglish/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.fishestlife.english;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CF999CA2300BBDE4004D66A6 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
@@ -178,16 +255,18 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CF43E872300A7CFB00CB9AE3 /* Release */ = {
|
||||
CF999CA3300BBDE4004D66A6 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
@@ -234,123 +313,38 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CF43E874300A7CFB00CB9AE3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 37HGUEC495;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 26;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.fishestlife.english;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = auto;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||
XROS_DEPLOYMENT_TARGET = 26;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CF43E875300A7CFB00CB9AE3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 37HGUEC495;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
|
||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 26;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.fishestlife.english;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
SDKROOT = auto;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||
XROS_DEPLOYMENT_TARGET = 26;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CF43E863300A7CFA00CB9AE3 /* Build configuration list for PBXProject "LearnEnglish" */ = {
|
||||
CF999C87300BBDE3004D66A6 /* Build configuration list for PBXProject "LearnEnglish" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CF43E871300A7CFB00CB9AE3 /* Debug */,
|
||||
CF43E872300A7CFB00CB9AE3 /* Release */,
|
||||
CF999CA2300BBDE4004D66A6 /* Debug */,
|
||||
CF999CA3300BBDE4004D66A6 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CF43E873300A7CFB00CB9AE3 /* Build configuration list for PBXNativeTarget "LearnEnglish" */ = {
|
||||
CF999C9F300BBDE4004D66A6 /* Build configuration list for PBXNativeTarget "LearnEnglish" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CF43E874300A7CFB00CB9AE3 /* Debug */,
|
||||
CF43E875300A7CFB00CB9AE3 /* Release */,
|
||||
CF999CA0300BBDE4004D66A6 /* Debug */,
|
||||
CF999CA1300BBDE4004D66A6 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CF43E860300A7CFA00CB9AE3 /* Project object */;
|
||||
rootObject = CF999C84300BBDE3004D66A6 /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// LearnEnglish
|
||||
//
|
||||
// Created by fish on 2026/7/18.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -27,57 +27,6 @@
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "logo 3.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 286 KiB |
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,26 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var store = WordStore()
|
||||
@State private var speech = SpeechService()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let error = store.loadError {
|
||||
ContentUnavailableView(
|
||||
"词库加载失败",
|
||||
systemImage: "exclamationmark.triangle",
|
||||
description: Text(error)
|
||||
)
|
||||
} else {
|
||||
CategoryListView(categories: store.categories)
|
||||
}
|
||||
}
|
||||
.environment(speech)
|
||||
.onAppear { store.load() }
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// LearnEnglishApp.swift
|
||||
// LearnEnglish
|
||||
//
|
||||
// Created by fish on 2026/7/17.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct LearnEnglishApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
enum AppRoute: Hashable {
|
||||
case category(WordCategory)
|
||||
case word(Word)
|
||||
case quizSetup(QuizSetupRequest)
|
||||
case quiz(QuizConfig)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// SceneDelegate.swift
|
||||
// LearnEnglish
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
let store = WordStore()
|
||||
store.load()
|
||||
let root = CategoryListViewController(store: store)
|
||||
window.rootViewController = UINavigationController(rootViewController: root)
|
||||
window.makeKeyAndVisible()
|
||||
self.window = window
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import AVFoundation
|
||||
|
||||
@Observable
|
||||
final class SpeechService: NSObject {
|
||||
static let shared = SpeechService()
|
||||
|
||||
private let synthesizer = AVSpeechSynthesizer()
|
||||
private(set) var speakingText: String?
|
||||
var onSpeakingChanged: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -18,6 +20,7 @@ final class SpeechService: NSObject {
|
||||
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
|
||||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.9
|
||||
speakingText = text
|
||||
onSpeakingChanged?()
|
||||
synthesizer.speak(utterance)
|
||||
}
|
||||
}
|
||||
@@ -25,9 +28,11 @@ final class SpeechService: NSObject {
|
||||
extension SpeechService: AVSpeechSynthesizerDelegate {
|
||||
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
|
||||
speakingText = nil
|
||||
onSpeakingChanged?()
|
||||
}
|
||||
|
||||
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
|
||||
speakingText = nil
|
||||
onSpeakingChanged?()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ enum WordStoreError: Error, LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
@Observable
|
||||
final class WordStore {
|
||||
private(set) var bank: WordBank?
|
||||
private(set) var loadError: String?
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CategoryListView: View {
|
||||
let categories: [WordCategory]
|
||||
@State private var path: [AppRoute] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $path) {
|
||||
List(categories) { category in
|
||||
NavigationLink(value: AppRoute.category(category)) {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: category.icon)
|
||||
.font(.title2)
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(.tint, in: RoundedRectangle(cornerRadius: 10))
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(category.name)
|
||||
.font(.headline)
|
||||
Text("\(category.words.count) 个单词")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.navigationTitle("单词分类")
|
||||
.navigationDestination(for: AppRoute.self) { route in
|
||||
switch route {
|
||||
case .category(let category):
|
||||
WordListView(category: category) { path.append($0) }
|
||||
case .word(let word):
|
||||
WordDetailView(word: word)
|
||||
case .quizSetup(let request):
|
||||
QuizSetupView(request: request)
|
||||
case .quiz(let config):
|
||||
QuizView(config: config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CategoryListView(categories: [
|
||||
WordCategory(id: "colors", name: "颜色", icon: "paintpalette.fill", words: [
|
||||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。")
|
||||
])
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import UIKit
|
||||
|
||||
final class CategoryListViewController: UITableViewController {
|
||||
private let store: WordStore
|
||||
|
||||
init(store: WordStore) {
|
||||
self.store = store
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "单词分类"
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
if let error = store.loadError {
|
||||
showError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func showError(_ message: String) {
|
||||
let label = UILabel()
|
||||
label.text = "词库加载失败\n\(message)"
|
||||
label.numberOfLines = 0
|
||||
label.textAlignment = .center
|
||||
label.textColor = .secondaryLabel
|
||||
let background = UIView()
|
||||
background.addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: background.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: background.centerYAnchor),
|
||||
label.leadingAnchor.constraint(greaterThanOrEqualTo: background.leadingAnchor, constant: 24)
|
||||
])
|
||||
tableView.backgroundView = background
|
||||
tableView.separatorStyle = .none
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
store.categories.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let category = store.categories[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = category.name
|
||||
content.textProperties.font = .preferredFont(forTextStyle: .headline)
|
||||
content.secondaryText = "\(category.words.count) 个单词"
|
||||
content.secondaryTextProperties.color = .secondaryLabel
|
||||
content.image = UIImage(systemName: category.icon)
|
||||
content.imageProperties.tintColor = .tintColor
|
||||
content.imageProperties.preferredSymbolConfiguration = .init(font: .preferredFont(forTextStyle: .title2))
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let category = store.categories[indexPath.row]
|
||||
navigationController?.pushViewController(WordListViewController(category: category), animated: true)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct QuizResultView: View {
|
||||
let total: Int
|
||||
let wrongWords: [Word]
|
||||
let duration: TimeInterval
|
||||
let onRestart: () -> Void
|
||||
let onExit: () -> Void
|
||||
final class QuizResultView: UIView {
|
||||
private let total: Int
|
||||
private let wrongWords: [Word]
|
||||
private let duration: TimeInterval
|
||||
private let onRestart: () -> Void
|
||||
private let onExit: () -> Void
|
||||
private let onSelectWord: (Word) -> Void
|
||||
|
||||
private var correctCount: Int { total - wrongWords.count }
|
||||
|
||||
@@ -14,57 +15,140 @@ struct QuizResultView: View {
|
||||
return seconds >= 60 ? "\(seconds / 60) 分 \(seconds % 60) 秒" : "\(seconds) 秒"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
VStack(spacing: 8) {
|
||||
Text("\(correctCount) / \(total)")
|
||||
.font(.system(size: 44, weight: .bold))
|
||||
Text("用时 \(durationText)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
init(total: Int, wrongWords: [Word], duration: TimeInterval,
|
||||
onRestart: @escaping () -> Void, onExit: @escaping () -> Void,
|
||||
onSelectWord: @escaping (Word) -> Void) {
|
||||
self.total = total
|
||||
self.wrongWords = wrongWords
|
||||
self.duration = duration
|
||||
self.onRestart = onRestart
|
||||
self.onExit = onExit
|
||||
self.onSelectWord = onSelectWord
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .systemBackground
|
||||
buildLayout()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
private func buildLayout() {
|
||||
let scrollView = UIScrollView()
|
||||
addSubview(scrollView)
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 24
|
||||
scrollView.addSubview(stack)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 24),
|
||||
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
||||
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -24),
|
||||
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32)
|
||||
])
|
||||
|
||||
let scoreLabel = UILabel()
|
||||
scoreLabel.text = "\(correctCount) / \(total)"
|
||||
scoreLabel.font = .systemFont(ofSize: 44, weight: .bold)
|
||||
scoreLabel.textAlignment = .center
|
||||
|
||||
let durationLabel = UILabel()
|
||||
durationLabel.text = "用时 \(durationText)"
|
||||
durationLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
durationLabel.textColor = .secondaryLabel
|
||||
durationLabel.textAlignment = .center
|
||||
|
||||
let scoreStack = UIStackView(arrangedSubviews: [scoreLabel, durationLabel])
|
||||
scoreStack.axis = .vertical
|
||||
scoreStack.spacing = 8
|
||||
stack.addArrangedSubview(scoreStack)
|
||||
|
||||
if !wrongWords.isEmpty {
|
||||
Section("错题(\(wrongWords.count))") {
|
||||
ForEach(wrongWords) { word in
|
||||
NavigationLink(value: AppRoute.word(word)) {
|
||||
HStack {
|
||||
Text(word.word)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(word.meaning)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stack.addArrangedSubview(makeWrongWordsSection())
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("再来一轮", action: onRestart)
|
||||
.frame(maxWidth: .infinity)
|
||||
Button("返回", action: onExit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
let restartButton = makeButton(title: "再来一轮", prominent: true) { [weak self] in
|
||||
self?.onRestart()
|
||||
}
|
||||
let exitButton = makeButton(title: "返回", prominent: false) { [weak self] in
|
||||
self?.onExit()
|
||||
}
|
||||
let buttonStack = UIStackView(arrangedSubviews: [restartButton, exitButton])
|
||||
buttonStack.axis = .vertical
|
||||
buttonStack.spacing = 12
|
||||
stack.addArrangedSubview(buttonStack)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
QuizResultView(
|
||||
total: 10,
|
||||
wrongWords: [
|
||||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。")
|
||||
],
|
||||
duration: 75,
|
||||
onRestart: {},
|
||||
onExit: {}
|
||||
)
|
||||
private func makeWrongWordsSection() -> UIView {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "错题(\(wrongWords.count))"
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
|
||||
let section = UIStackView(arrangedSubviews: [titleLabel])
|
||||
section.axis = .vertical
|
||||
section.spacing = 8
|
||||
|
||||
for word in wrongWords {
|
||||
let button = UIButton(type: .system)
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.contentInsets = .init(top: 8, leading: 12, bottom: 8, trailing: 12)
|
||||
button.configuration = config
|
||||
button.contentHorizontalAlignment = .fill
|
||||
|
||||
let wordLabel = UILabel()
|
||||
wordLabel.text = word.word
|
||||
wordLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
let meaningLabel = UILabel()
|
||||
meaningLabel.text = word.meaning
|
||||
meaningLabel.textColor = .secondaryLabel
|
||||
let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||||
chevron.tintColor = .tertiaryLabel
|
||||
chevron.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
let row = UIStackView(arrangedSubviews: [wordLabel, UIView(), meaningLabel, chevron])
|
||||
row.axis = .horizontal
|
||||
row.alignment = .center
|
||||
row.spacing = 8
|
||||
row.isUserInteractionEnabled = false
|
||||
button.addSubview(row)
|
||||
row.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
row.topAnchor.constraint(equalTo: button.topAnchor, constant: 8),
|
||||
row.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
|
||||
row.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: -12),
|
||||
row.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -8)
|
||||
])
|
||||
|
||||
button.addAction(UIAction { [weak self] _ in
|
||||
self?.onSelectWord(word)
|
||||
}, for: .touchUpInside)
|
||||
section.addArrangedSubview(button)
|
||||
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = .separator
|
||||
divider.translatesAutoresizingMaskIntoConstraints = false
|
||||
divider.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale).isActive = true
|
||||
section.addArrangedSubview(divider)
|
||||
}
|
||||
return section
|
||||
}
|
||||
|
||||
private func makeButton(title: String, prominent: Bool, action: @escaping () -> Void) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
var config = prominent ? UIButton.Configuration.borderedProminent() : UIButton.Configuration.plain()
|
||||
config.title = title
|
||||
config.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16)
|
||||
button.configuration = config
|
||||
button.addAction(UIAction { _ in action() }, for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct QuizSetupView: View {
|
||||
let request: QuizSetupRequest
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
NavigationLink(value: AppRoute.quiz(QuizConfig(mode: request.mode, words: request.words))) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("全部随机测试")
|
||||
.font(.headline)
|
||||
Text("\(request.words.count) 题")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
NavigationLink(value: AppRoute.quiz(QuizConfig(mode: request.mode, words: Array(request.words.shuffled().prefix(10))))) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("随机十个")
|
||||
.font(.headline)
|
||||
Text("从词库中随机抽取 10 题")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.navigationTitle(request.mode.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
QuizSetupView(request: QuizSetupRequest(mode: .enToZh, words: [
|
||||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。")
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import UIKit
|
||||
|
||||
final class QuizSetupViewController: UITableViewController {
|
||||
private let request: QuizSetupRequest
|
||||
|
||||
init(request: QuizSetupRequest) {
|
||||
self.request = request
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = request.mode.rawValue
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 2 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.textProperties.font = .preferredFont(forTextStyle: .headline)
|
||||
content.secondaryTextProperties.color = .secondaryLabel
|
||||
if indexPath.row == 0 {
|
||||
content.text = "全部随机测试"
|
||||
content.secondaryText = "\(request.words.count) 题"
|
||||
} else {
|
||||
content.text = "随机十个"
|
||||
content.secondaryText = "从词库中随机抽取 10 题"
|
||||
}
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let words = indexPath.row == 0 ? request.words : Array(request.words.shuffled().prefix(10))
|
||||
navigationController?.pushViewController(QuizViewController(config: QuizConfig(mode: request.mode, words: words)), animated: true)
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct QuizView: View {
|
||||
let config: QuizConfig
|
||||
|
||||
@Environment(SpeechService.self) private var speech
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var questions: [QuizQuestion]
|
||||
@State private var currentIndex = 0
|
||||
@State private var selectedOption: String?
|
||||
@State private var userInput = ""
|
||||
@State private var wrongWords: [Word] = []
|
||||
@State private var finished = false
|
||||
@State private var startDate = Date.now
|
||||
|
||||
init(config: QuizConfig) {
|
||||
self.config = config
|
||||
_questions = State(initialValue: QuizGenerator.makeQuestions(mode: config.mode, words: config.words))
|
||||
}
|
||||
|
||||
private var current: QuizQuestion? {
|
||||
questions.indices.contains(currentIndex) ? questions[currentIndex] : nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if finished {
|
||||
QuizResultView(
|
||||
total: questions.count,
|
||||
wrongWords: wrongWords,
|
||||
duration: Date.now.timeIntervalSince(startDate),
|
||||
onRestart: restart,
|
||||
onExit: { dismiss() }
|
||||
)
|
||||
} else if let question = current {
|
||||
questionView(question)
|
||||
}
|
||||
}
|
||||
.navigationTitle(config.mode.rawValue)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func questionView(_ question: QuizQuestion) -> some View {
|
||||
VStack(spacing: 24) {
|
||||
VStack(spacing: 8) {
|
||||
ProgressView(value: Double(currentIndex), total: Double(questions.count))
|
||||
Text("第 \(currentIndex + 1) / \(questions.count) 题")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Text(question.prompt)
|
||||
.font(.system(size: config.mode == .enToZh ? 44 : 34, weight: .bold))
|
||||
if config.mode == .enToZh {
|
||||
Text(question.word.phonetic)
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
Button {
|
||||
speech.speak(question.prompt)
|
||||
} label: {
|
||||
Image(systemName: "speaker.wave.2.fill")
|
||||
.font(.title2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
if config.mode == .spelling {
|
||||
spellingView(question)
|
||||
} else {
|
||||
optionsView(question)
|
||||
}
|
||||
|
||||
if let selectedOption {
|
||||
VStack(spacing: 12) {
|
||||
if config.mode == .spelling {
|
||||
Text(selectedOption == question.answer ? "正确" : "正确答案:\(question.answer)")
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
||||
Text(question.word.phonetic)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text(selectedOption == question.answer ? "回答正确" : "正确答案:\(question.answer)")
|
||||
.font(.headline)
|
||||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
||||
}
|
||||
Button {
|
||||
next()
|
||||
} label: {
|
||||
Text(currentIndex + 1 < questions.count ? "下一题" : "查看成绩")
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func optionsView(_ question: QuizQuestion) -> some View {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(question.options, id: \.self) { option in
|
||||
Button {
|
||||
select(option, for: question)
|
||||
} label: {
|
||||
VStack(spacing: 4) {
|
||||
Text(option)
|
||||
.font(.headline)
|
||||
if config.mode == .zhToEn, let w = config.words.first(where: { $0.word == option }) {
|
||||
Text(w.phonetic)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(optionColor(option, for: question))
|
||||
.disabled(selectedOption != nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func spellingView(_ question: QuizQuestion) -> some View {
|
||||
VStack(spacing: 24) {
|
||||
Text(question.prompt)
|
||||
.font(.title.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(question.blankedWord.enumerated()), id: \.offset) { i, ch in
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.quaternary.opacity(0.15))
|
||||
.frame(width: 36, height: 44)
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(.quaternary, lineWidth: 1)
|
||||
.frame(width: 36, height: 44)
|
||||
if ch == "_" {
|
||||
Rectangle()
|
||||
.fill(.secondary.opacity(0.5))
|
||||
.frame(width: 16, height: 2)
|
||||
.offset(y: 12)
|
||||
} else {
|
||||
Text(ch.uppercased())
|
||||
.font(.title2.weight(.bold))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("\(question.word.word.count) 个字母,填写完整单词")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
|
||||
TextField("在此输入完整单词", text: $userInput)
|
||||
.font(.body)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.disabled(selectedOption != nil)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 14)
|
||||
.background(.quaternary.opacity(0.2), in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.quaternary, lineWidth: 1)
|
||||
)
|
||||
|
||||
Button("确认") {
|
||||
let answer = userInput.trimmingCharacters(in: .whitespaces).lowercased()
|
||||
guard !answer.isEmpty else { return }
|
||||
selectedOption = answer
|
||||
if answer != question.answer {
|
||||
wrongWords.append(question.word)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.disabled(userInput.trimmingCharacters(in: .whitespaces).isEmpty || selectedOption != nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func optionColor(_ option: String, for question: QuizQuestion) -> Color {
|
||||
guard let selectedOption else { return .accentColor }
|
||||
if option == question.answer { return .green }
|
||||
if option == selectedOption { return .red }
|
||||
return .gray
|
||||
}
|
||||
|
||||
private func select(_ option: String, for question: QuizQuestion) {
|
||||
guard selectedOption == nil else { return }
|
||||
selectedOption = option
|
||||
if option != question.answer {
|
||||
wrongWords.append(question.word)
|
||||
}
|
||||
}
|
||||
|
||||
private func next() {
|
||||
if currentIndex + 1 < questions.count {
|
||||
currentIndex += 1
|
||||
selectedOption = nil
|
||||
userInput = ""
|
||||
} else {
|
||||
finished = true
|
||||
}
|
||||
}
|
||||
|
||||
private func restart() {
|
||||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||
currentIndex = 0
|
||||
selectedOption = nil
|
||||
userInput = ""
|
||||
wrongWords = []
|
||||
finished = false
|
||||
startDate = .now
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
QuizView(config: QuizConfig(mode: .enToZh, words: [
|
||||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。"),
|
||||
Word(word: "blue", phonetic: "/bluː/", meaning: "蓝色", example: "The sky is blue.", exampleMeaning: "天空是蓝色的。"),
|
||||
Word(word: "green", phonetic: "/ɡriːn/", meaning: "绿色", example: "The grass is green.", exampleMeaning: "草地是绿色的。"),
|
||||
Word(word: "yellow", phonetic: "/ˈjeloʊ/", meaning: "黄色", example: "The banana is yellow.", exampleMeaning: "香蕉是黄色的。")
|
||||
]))
|
||||
.environment(SpeechService())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import UIKit
|
||||
|
||||
final class QuizViewController: UIViewController {
|
||||
private let config: QuizConfig
|
||||
private let speech = SpeechService.shared
|
||||
|
||||
private var questions: [QuizQuestion]
|
||||
private var currentIndex = 0
|
||||
private var selectedOption: String?
|
||||
private var wrongWords: [Word] = []
|
||||
private var startDate = Date.now
|
||||
|
||||
private let progressView = UIProgressView(progressViewStyle: .default)
|
||||
private let countLabel = UILabel()
|
||||
private let promptLabel = UILabel()
|
||||
private let phoneticLabel = UILabel()
|
||||
private let speakerButton = UIButton(type: .system)
|
||||
private let optionsStack = UIStackView()
|
||||
private let spellingView = SpellingInputView()
|
||||
private let spellingHintLabel = UILabel()
|
||||
private let resultLabel = UILabel()
|
||||
private let resultPhoneticLabel = UILabel()
|
||||
private let nextButton = UIButton(type: .system)
|
||||
private var optionButtons: [UIButton] = []
|
||||
|
||||
private var current: QuizQuestion? {
|
||||
questions.indices.contains(currentIndex) ? questions[currentIndex] : nil
|
||||
}
|
||||
|
||||
init(config: QuizConfig) {
|
||||
self.config = config
|
||||
self.questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
title = config.mode.rawValue
|
||||
buildLayout()
|
||||
showQuestion()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if config.mode == .spelling, selectedOption == nil {
|
||||
spellingView.activate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func buildLayout() {
|
||||
countLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
countLabel.textColor = .secondaryLabel
|
||||
countLabel.textAlignment = .center
|
||||
|
||||
promptLabel.font = .systemFont(ofSize: config.mode == .enToZh ? 44 : 34, weight: .bold)
|
||||
promptLabel.textAlignment = .center
|
||||
promptLabel.numberOfLines = 0
|
||||
|
||||
phoneticLabel.font = .preferredFont(forTextStyle: .title3)
|
||||
phoneticLabel.textColor = .secondaryLabel
|
||||
phoneticLabel.textAlignment = .center
|
||||
|
||||
speakerButton.setImage(UIImage(systemName: "speaker.wave.2.fill"), for: .normal)
|
||||
speakerButton.addAction(UIAction { [weak self] _ in
|
||||
guard let self, let question = current else { return }
|
||||
speech.speak(question.prompt)
|
||||
}, for: .touchUpInside)
|
||||
|
||||
optionsStack.axis = .vertical
|
||||
optionsStack.spacing = 12
|
||||
|
||||
spellingHintLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
spellingHintLabel.textColor = .tertiaryLabel
|
||||
spellingHintLabel.textAlignment = .center
|
||||
|
||||
resultLabel.font = .preferredFont(forTextStyle: .title2).withWeight(.bold)
|
||||
resultLabel.textAlignment = .center
|
||||
|
||||
resultPhoneticLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
resultPhoneticLabel.textColor = .secondaryLabel
|
||||
resultPhoneticLabel.textAlignment = .center
|
||||
|
||||
var nextConfig = UIButton.Configuration.borderedProminent()
|
||||
nextConfig.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16)
|
||||
nextButton.configuration = nextConfig
|
||||
nextButton.addAction(UIAction { [weak self] _ in self?.next() }, for: .touchUpInside)
|
||||
|
||||
let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel])
|
||||
headerStack.axis = .vertical
|
||||
headerStack.spacing = 8
|
||||
|
||||
let promptStack = UIStackView(arrangedSubviews: [promptLabel, phoneticLabel, speakerButton])
|
||||
promptStack.axis = .vertical
|
||||
promptStack.spacing = 12
|
||||
promptStack.alignment = .center
|
||||
|
||||
let inputStack = UIStackView(arrangedSubviews: [optionsStack, spellingView, spellingHintLabel])
|
||||
inputStack.axis = .vertical
|
||||
inputStack.spacing = 16
|
||||
|
||||
let resultStack = UIStackView(arrangedSubviews: [resultLabel, resultPhoneticLabel, nextButton])
|
||||
resultStack.axis = .vertical
|
||||
resultStack.spacing = 12
|
||||
|
||||
let mainStack = UIStackView(arrangedSubviews: [headerStack, promptStack, inputStack, resultStack, UIView()])
|
||||
mainStack.axis = .vertical
|
||||
mainStack.spacing = 24
|
||||
view.addSubview(mainStack)
|
||||
mainStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
mainStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
|
||||
mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||
mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
||||
mainStack.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Question flow
|
||||
|
||||
private func showQuestion() {
|
||||
guard let question = current else { return }
|
||||
selectedOption = nil
|
||||
|
||||
progressView.progress = Float(currentIndex) / Float(questions.count)
|
||||
countLabel.text = "第 \(currentIndex + 1) / \(questions.count) 题"
|
||||
promptLabel.text = question.prompt
|
||||
phoneticLabel.isHidden = config.mode != .enToZh
|
||||
speakerButton.isHidden = config.mode != .enToZh
|
||||
phoneticLabel.text = question.word.phonetic
|
||||
|
||||
resultLabel.isHidden = true
|
||||
resultPhoneticLabel.isHidden = true
|
||||
nextButton.isHidden = true
|
||||
|
||||
let isSpelling = config.mode == .spelling
|
||||
optionsStack.isHidden = isSpelling
|
||||
spellingView.isHidden = !isSpelling
|
||||
spellingHintLabel.isHidden = !isSpelling
|
||||
|
||||
if isSpelling {
|
||||
spellingHintLabel.text = "\(question.word.word.count) 个字母,补全空缺部分"
|
||||
spellingView.configure(blankedWord: question.blankedWord, blankPositions: question.blankPositions)
|
||||
spellingView.onComplete = { [weak self] typed in
|
||||
self?.submitSpelling(typed, for: question)
|
||||
}
|
||||
spellingView.activate()
|
||||
} else {
|
||||
buildOptionButtons(for: question)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildOptionButtons(for question: QuizQuestion) {
|
||||
optionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
optionButtons = question.options.map { option in
|
||||
let button = UIButton(type: .system)
|
||||
var config = UIButton.Configuration.bordered()
|
||||
config.title = option
|
||||
config.titleTextAttributesTransformer = .init { incoming in
|
||||
var outgoing = incoming
|
||||
outgoing.font = .preferredFont(forTextStyle: .headline)
|
||||
return outgoing
|
||||
}
|
||||
if self.config.mode == .zhToEn, let w = self.config.words.first(where: { $0.word == option }) {
|
||||
config.subtitle = w.phonetic
|
||||
}
|
||||
config.contentInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16)
|
||||
button.configuration = config
|
||||
button.addAction(UIAction { [weak self] _ in
|
||||
self?.selectOption(option, for: question)
|
||||
}, for: .touchUpInside)
|
||||
optionsStack.addArrangedSubview(button)
|
||||
return button
|
||||
}
|
||||
}
|
||||
|
||||
private func selectOption(_ option: String, for question: QuizQuestion) {
|
||||
guard selectedOption == nil else { return }
|
||||
selectedOption = option
|
||||
if option != question.answer {
|
||||
wrongWords.append(question.word)
|
||||
}
|
||||
for (index, button) in optionButtons.enumerated() {
|
||||
let value = question.options[index]
|
||||
button.isEnabled = false
|
||||
if value == question.answer {
|
||||
button.configuration?.baseForegroundColor = .systemGreen
|
||||
} else if value == option {
|
||||
button.configuration?.baseForegroundColor = .systemRed
|
||||
} else {
|
||||
button.configuration?.baseForegroundColor = .systemGray
|
||||
}
|
||||
}
|
||||
showResult(option == question.answer, for: question)
|
||||
}
|
||||
|
||||
private func submitSpelling(_ typed: String, for question: QuizQuestion) {
|
||||
guard selectedOption == nil else { return }
|
||||
selectedOption = typed
|
||||
if typed != question.answer {
|
||||
wrongWords.append(question.word)
|
||||
}
|
||||
spellingView.deactivate()
|
||||
showResult(typed == question.answer, for: question)
|
||||
}
|
||||
|
||||
private func showResult(_ correct: Bool, for question: QuizQuestion) {
|
||||
if config.mode == .spelling {
|
||||
resultLabel.text = correct ? "正确" : "正确答案:\(question.answer)"
|
||||
resultPhoneticLabel.text = question.word.phonetic
|
||||
resultPhoneticLabel.isHidden = false
|
||||
} else {
|
||||
resultLabel.text = correct ? "回答正确" : "正确答案:\(question.answer)"
|
||||
}
|
||||
resultLabel.textColor = correct ? .systemGreen : .systemRed
|
||||
resultLabel.isHidden = false
|
||||
|
||||
nextButton.configuration?.title = currentIndex + 1 < questions.count ? "下一题" : "查看成绩"
|
||||
nextButton.isHidden = false
|
||||
}
|
||||
|
||||
private func next() {
|
||||
if currentIndex + 1 < questions.count {
|
||||
currentIndex += 1
|
||||
showQuestion()
|
||||
} else {
|
||||
showQuizResult()
|
||||
}
|
||||
}
|
||||
|
||||
private func showQuizResult() {
|
||||
let resultView = QuizResultView(
|
||||
total: questions.count,
|
||||
wrongWords: wrongWords,
|
||||
duration: Date.now.timeIntervalSince(startDate),
|
||||
onRestart: { [weak self] in self?.restart() },
|
||||
onExit: { [weak self] in self?.navigationController?.popViewController(animated: true) },
|
||||
onSelectWord: { [weak self] word in
|
||||
self?.navigationController?.pushViewController(WordDetailViewController(word: word), animated: true)
|
||||
}
|
||||
)
|
||||
resultView.translatesAutoresizingMaskIntoConstraints = false
|
||||
resultView.frame = view.bounds
|
||||
resultView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(resultView)
|
||||
}
|
||||
|
||||
private func restart() {
|
||||
view.subviews.compactMap { $0 as? QuizResultView }.forEach { $0.removeFromSuperview() }
|
||||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||
currentIndex = 0
|
||||
wrongWords = []
|
||||
startDate = .now
|
||||
showQuestion()
|
||||
if config.mode == .spelling {
|
||||
spellingView.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIFont {
|
||||
func withWeight(_ weight: UIFont.Weight) -> UIFont {
|
||||
let descriptor = fontDescriptor.addingAttributes([
|
||||
.traits: [UIFontDescriptor.TraitKey.weight: weight]
|
||||
])
|
||||
return UIFont(descriptor: descriptor, size: pointSize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import UIKit
|
||||
|
||||
final class SpellingInputView: UIView, UITextFieldDelegate {
|
||||
var onComplete: ((String) -> Void)?
|
||||
|
||||
private let hiddenField = UITextField()
|
||||
private let stack = UIStackView()
|
||||
|
||||
private var chars: [String] = []
|
||||
private var blankPositions: [Int] = []
|
||||
private var filledBlanks: [String] = []
|
||||
private var currentBlankIndex = 0
|
||||
private var boxViews: [UIView] = []
|
||||
private var cursorView: UIView?
|
||||
private var inputEnabled = true
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 6
|
||||
stack.alignment = .center
|
||||
addSubview(stack)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: topAnchor),
|
||||
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
stack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
stack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
|
||||
stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
|
||||
])
|
||||
|
||||
hiddenField.delegate = self
|
||||
hiddenField.autocorrectionType = .no
|
||||
hiddenField.autocapitalizationType = .none
|
||||
hiddenField.spellCheckingType = .no
|
||||
hiddenField.keyboardType = .asciiCapable
|
||||
hiddenField.frame = .zero
|
||||
addSubview(hiddenField)
|
||||
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reactivate)))
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func configure(blankedWord: String, blankPositions: [Int]) {
|
||||
chars = blankedWord.map { String($0) }
|
||||
self.blankPositions = blankPositions
|
||||
filledBlanks = Array(repeating: "", count: blankPositions.count)
|
||||
currentBlankIndex = 0
|
||||
inputEnabled = true
|
||||
rebuildBoxes()
|
||||
}
|
||||
|
||||
func activate() {
|
||||
guard inputEnabled, !hiddenField.isFirstResponder else { return }
|
||||
hiddenField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
inputEnabled = false
|
||||
hiddenField.resignFirstResponder()
|
||||
refreshBoxes()
|
||||
}
|
||||
|
||||
@objc private func reactivate() {
|
||||
activate()
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard inputEnabled else { return false }
|
||||
if string.isEmpty {
|
||||
if currentBlankIndex > 0 {
|
||||
currentBlankIndex -= 1
|
||||
filledBlanks[currentBlankIndex] = ""
|
||||
refreshBoxes()
|
||||
}
|
||||
} else if let char = string.last, char.isLetter, currentBlankIndex < blankPositions.count {
|
||||
filledBlanks[currentBlankIndex] = String(char).lowercased()
|
||||
currentBlankIndex += 1
|
||||
refreshBoxes()
|
||||
if currentBlankIndex == blankPositions.count {
|
||||
var rebuilt = chars
|
||||
for (i, pos) in blankPositions.enumerated() {
|
||||
rebuilt[pos] = filledBlanks[i]
|
||||
}
|
||||
onComplete?(rebuilt.joined().lowercased())
|
||||
}
|
||||
}
|
||||
textField.text = nil
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Boxes
|
||||
|
||||
private func rebuildBoxes() {
|
||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
boxViews = chars.enumerated().map { index, ch in
|
||||
let box = makeBox()
|
||||
if ch != "_", let label = box.subviews.compactMap({ $0 as? UILabel }).first {
|
||||
label.text = ch.uppercased()
|
||||
label.isHidden = false
|
||||
}
|
||||
stack.addArrangedSubview(box)
|
||||
NSLayoutConstraint.activate([
|
||||
box.widthAnchor.constraint(equalToConstant: 36),
|
||||
box.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
return box
|
||||
}
|
||||
refreshBoxes()
|
||||
}
|
||||
|
||||
private func makeBox() -> UIView {
|
||||
let box = UIView()
|
||||
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
|
||||
box.layer.cornerRadius = 8
|
||||
box.layer.borderWidth = 1
|
||||
box.layer.borderColor = UIColor.separator.cgColor
|
||||
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .title2)
|
||||
label.font = .systemFont(ofSize: label.font.pointSize, weight: .bold)
|
||||
label.isHidden = true
|
||||
label.tag = 100
|
||||
box.addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: box.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: box.centerYAnchor)
|
||||
])
|
||||
return box
|
||||
}
|
||||
|
||||
private func refreshBoxes() {
|
||||
cursorView?.removeFromSuperview()
|
||||
cursorView = nil
|
||||
|
||||
for (index, box) in boxViews.enumerated() {
|
||||
guard chars[index] == "_", let blankIndex = blankPositions.firstIndex(of: index) else {
|
||||
continue
|
||||
}
|
||||
let label = box.viewWithTag(100) as? UILabel
|
||||
let filled = filledBlanks[blankIndex]
|
||||
label?.text = filled.uppercased()
|
||||
label?.isHidden = filled.isEmpty
|
||||
|
||||
let isCurrent = inputEnabled && blankIndex == currentBlankIndex
|
||||
box.layer.borderColor = isCurrent ? UIColor.tintColor.cgColor : UIColor.separator.cgColor
|
||||
box.layer.borderWidth = isCurrent ? 2 : 1
|
||||
|
||||
if isCurrent, filled.isEmpty {
|
||||
let cursor = UIView()
|
||||
cursor.backgroundColor = .tintColor
|
||||
box.addSubview(cursor)
|
||||
cursor.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
cursor.centerXAnchor.constraint(equalTo: box.centerXAnchor),
|
||||
cursor.centerYAnchor.constraint(equalTo: box.centerYAnchor),
|
||||
cursor.widthAnchor.constraint(equalToConstant: 2),
|
||||
cursor.heightAnchor.constraint(equalToConstant: 22)
|
||||
])
|
||||
cursorView = cursor
|
||||
startBlinking(cursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startBlinking(_ cursor: UIView) {
|
||||
cursor.alpha = 1
|
||||
UIView.animate(withDuration: 0.6, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction]) {
|
||||
cursor.alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WordDetailView: View {
|
||||
let word: Word
|
||||
@Environment(SpeechService.self) private var speech
|
||||
|
||||
private var isSpeaking: Bool {
|
||||
speech.speakingText == word.word
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 32) {
|
||||
VStack(spacing: 12) {
|
||||
Text(word.word)
|
||||
.font(.system(size: 48, weight: .bold))
|
||||
Text(word.phonetic)
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
Button {
|
||||
speech.speak(word.word)
|
||||
} label: {
|
||||
Label(isSpeaking ? "朗读中" : "发音", systemImage: isSpeaking ? "speaker.wave.3.fill" : "speaker.wave.2.fill")
|
||||
.font(.headline)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isSpeaking)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("释义")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(word.meaning)
|
||||
.font(.title2)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("例句")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(word.example)
|
||||
.font(.body)
|
||||
Text(word.exampleMeaning)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
speech.speak(word.example)
|
||||
} label: {
|
||||
Image(systemName: "speaker.wave.2")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(word.word)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
WordDetailView(word: Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。"))
|
||||
.environment(SpeechService())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import UIKit
|
||||
|
||||
final class WordDetailViewController: UIViewController {
|
||||
private let word: Word
|
||||
private let speech = SpeechService.shared
|
||||
|
||||
private let speakButton = UIButton(type: .system)
|
||||
private var isSpeaking: Bool { speech.speakingText == word.word }
|
||||
|
||||
init(word: Word) {
|
||||
self.word = word
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
title = word.word
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
buildLayout()
|
||||
updateSpeakButton()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
speech.onSpeakingChanged = { [weak self] in
|
||||
self?.updateSpeakButton()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSpeakButton() {
|
||||
var config = speakButton.configuration
|
||||
config?.title = isSpeaking ? "朗读中" : "发音"
|
||||
config?.image = UIImage(systemName: isSpeaking ? "speaker.wave.3.fill" : "speaker.wave.2.fill")
|
||||
speakButton.configuration = config
|
||||
speakButton.isEnabled = !isSpeaking
|
||||
}
|
||||
|
||||
private func buildLayout() {
|
||||
let scrollView = UIScrollView()
|
||||
view.addSubview(scrollView)
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 32
|
||||
scrollView.addSubview(stack)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),
|
||||
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
|
||||
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
|
||||
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32)
|
||||
])
|
||||
|
||||
stack.addArrangedSubview(makeHeader())
|
||||
stack.addArrangedSubview(makeCard())
|
||||
}
|
||||
|
||||
private func makeHeader() -> UIView {
|
||||
let wordLabel = UILabel()
|
||||
wordLabel.text = word.word
|
||||
wordLabel.font = .systemFont(ofSize: 48, weight: .bold)
|
||||
wordLabel.textAlignment = .center
|
||||
|
||||
let phoneticLabel = UILabel()
|
||||
phoneticLabel.text = word.phonetic
|
||||
phoneticLabel.font = .preferredFont(forTextStyle: .title3)
|
||||
phoneticLabel.textColor = .secondaryLabel
|
||||
phoneticLabel.textAlignment = .center
|
||||
|
||||
var config = UIButton.Configuration.borderedProminent()
|
||||
config.imagePadding = 6
|
||||
config.contentInsets = .init(top: 10, leading: 24, bottom: 10, trailing: 24)
|
||||
speakButton.configuration = config
|
||||
speakButton.addAction(UIAction { [weak self] _ in
|
||||
guard let self else { return }
|
||||
speech.speak(word.word)
|
||||
}, for: .touchUpInside)
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [wordLabel, phoneticLabel, speakButton])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
stack.alignment = .center
|
||||
return stack
|
||||
}
|
||||
|
||||
private func makeCard() -> UIView {
|
||||
let card = UIView()
|
||||
card.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.5)
|
||||
card.layer.cornerRadius = 16
|
||||
|
||||
let meaningTitle = makeCaption("释义")
|
||||
let meaningLabel = UILabel()
|
||||
meaningLabel.text = word.meaning
|
||||
meaningLabel.font = .preferredFont(forTextStyle: .title2)
|
||||
meaningLabel.numberOfLines = 0
|
||||
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = .separator
|
||||
divider.translatesAutoresizingMaskIntoConstraints = false
|
||||
divider.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale).isActive = true
|
||||
|
||||
let exampleTitle = makeCaption("例句")
|
||||
let exampleLabel = UILabel()
|
||||
exampleLabel.text = word.example
|
||||
exampleLabel.numberOfLines = 0
|
||||
let exampleMeaningLabel = UILabel()
|
||||
exampleMeaningLabel.text = word.exampleMeaning
|
||||
exampleMeaningLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
exampleMeaningLabel.textColor = .secondaryLabel
|
||||
exampleMeaningLabel.numberOfLines = 0
|
||||
|
||||
let exampleSpeakButton = UIButton(type: .system)
|
||||
exampleSpeakButton.setImage(UIImage(systemName: "speaker.wave.2"), for: .normal)
|
||||
exampleSpeakButton.addAction(UIAction { [weak self] _ in
|
||||
guard let self else { return }
|
||||
speech.speak(word.example)
|
||||
}, for: .touchUpInside)
|
||||
exampleSpeakButton.setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
let exampleTextStack = UIStackView(arrangedSubviews: [exampleLabel, exampleMeaningLabel])
|
||||
exampleTextStack.axis = .vertical
|
||||
exampleTextStack.spacing = 4
|
||||
let exampleRow = UIStackView(arrangedSubviews: [exampleTextStack, exampleSpeakButton])
|
||||
exampleRow.axis = .horizontal
|
||||
exampleRow.alignment = .top
|
||||
exampleRow.spacing = 8
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [meaningTitle, meaningLabel, divider, exampleTitle, exampleRow])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 16
|
||||
stack.setCustomSpacing(6, after: meaningTitle)
|
||||
stack.setCustomSpacing(6, after: exampleTitle)
|
||||
|
||||
card.addSubview(stack)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
|
||||
stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
||||
stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16)
|
||||
])
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeCaption(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .preferredFont(forTextStyle: .caption1)
|
||||
label.textColor = .secondaryLabel
|
||||
return label
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WordListView: View {
|
||||
let category: WordCategory
|
||||
let onRoute: (AppRoute) -> Void
|
||||
|
||||
var body: some View {
|
||||
List(category.words) { word in
|
||||
NavigationLink(value: AppRoute.word(word)) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(word.word)
|
||||
.font(.headline)
|
||||
Text(word.meaning)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
.contentMargins(.top, 8, for: .scrollContent)
|
||||
.navigationTitle(category.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
Button("英选汉") { startQuiz(.enToZh) }
|
||||
Button("汉选英") { startQuiz(.zhToEn) }
|
||||
Button("拼写") { startQuiz(.spelling) }
|
||||
} label: {
|
||||
Text("测验")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startQuiz(_ mode: QuizMode) {
|
||||
if category.words.count <= 10 {
|
||||
onRoute(.quiz(QuizConfig(mode: mode, words: category.words)))
|
||||
} else {
|
||||
onRoute(.quizSetup(QuizSetupRequest(mode: mode, words: category.words)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
WordListView(category: WordCategory(id: "colors", name: "颜色", icon: "paintpalette.fill", words: [
|
||||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。")
|
||||
])) { _ in }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import UIKit
|
||||
|
||||
final class WordListViewController: UITableViewController {
|
||||
private let category: WordCategory
|
||||
|
||||
init(category: WordCategory) {
|
||||
self.category = category
|
||||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = category.name
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
|
||||
let quizMenu = UIMenu(title: "", children: [
|
||||
UIAction(title: QuizMode.enToZh.rawValue) { [weak self] _ in self?.startQuiz(.enToZh) },
|
||||
UIAction(title: QuizMode.zhToEn.rawValue) { [weak self] _ in self?.startQuiz(.zhToEn) },
|
||||
UIAction(title: QuizMode.spelling.rawValue) { [weak self] _ in self?.startQuiz(.spelling) }
|
||||
])
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "测验", menu: quizMenu)
|
||||
}
|
||||
|
||||
private func startQuiz(_ mode: QuizMode) {
|
||||
if category.words.count <= 10 {
|
||||
let config = QuizConfig(mode: mode, words: category.words)
|
||||
navigationController?.pushViewController(QuizViewController(config: config), animated: true)
|
||||
} else {
|
||||
let request = QuizSetupRequest(mode: mode, words: category.words)
|
||||
navigationController?.pushViewController(QuizSetupViewController(request: request), animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
category.words.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let word = category.words[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = word.word
|
||||
content.textProperties.font = .preferredFont(forTextStyle: .headline)
|
||||
content.secondaryText = word.meaning
|
||||
content.secondaryTextProperties.color = .secondaryLabel
|
||||
cell.contentConfiguration = content
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
navigationController?.pushViewController(WordDetailViewController(word: category.words[indexPath.row]), animated: true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user