Compare commits
8 Commits
b387e5ff80
...
ae33320f51
| Author | SHA1 | Date | |
|---|---|---|---|
| ae33320f51 | |||
| 7cc90dce10 | |||
| cf84d6cce0 | |||
| 9067ddb17c | |||
| a1c3194570 | |||
| f5dd7fa724 | |||
| 1a1563cd11 | |||
| dedc8f4bcc |
@@ -0,0 +1,356 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CF43E868300A7CFA00CB9AE3 /* LearnEnglish.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LearnEnglish.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
CF43E86A300A7CFA00CB9AE3 /* LearnEnglish */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = LearnEnglish;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CF43E865300A7CFA00CB9AE3 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CF43E85F300A7CFA00CB9AE3 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CF43E86A300A7CFA00CB9AE3 /* LearnEnglish */,
|
||||
CF43E869300A7CFA00CB9AE3 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CF43E869300A7CFA00CB9AE3 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CF43E868300A7CFA00CB9AE3 /* LearnEnglish.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CF43E867300A7CFA00CB9AE3 /* LearnEnglish */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CF43E873300A7CFB00CB9AE3 /* Build configuration list for PBXNativeTarget "LearnEnglish" */;
|
||||
buildPhases = (
|
||||
CF43E864300A7CFA00CB9AE3 /* Sources */,
|
||||
CF43E865300A7CFA00CB9AE3 /* Frameworks */,
|
||||
CF43E866300A7CFA00CB9AE3 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
CF43E86A300A7CFA00CB9AE3 /* LearnEnglish */,
|
||||
);
|
||||
name = LearnEnglish;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = LearnEnglish;
|
||||
productReference = CF43E868300A7CFA00CB9AE3 /* LearnEnglish.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CF43E860300A7CFA00CB9AE3 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2630;
|
||||
LastUpgradeCheck = 2630;
|
||||
TargetAttributes = {
|
||||
CF43E867300A7CFA00CB9AE3 = {
|
||||
CreatedOnToolsVersion = 26.3;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CF43E863300A7CFA00CB9AE3 /* Build configuration list for PBXProject "LearnEnglish" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CF43E85F300A7CFA00CB9AE3;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = CF43E869300A7CFA00CB9AE3 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CF43E867300A7CFA00CB9AE3 /* LearnEnglish */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CF43E866300A7CFA00CB9AE3 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CF43E864300A7CFA00CB9AE3 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CF43E871300A7CFB00CB9AE3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 37HGUEC495;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CF43E872300A7CFB00CB9AE3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 37HGUEC495;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
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;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CF43E863300A7CFA00CB9AE3 /* Build configuration list for PBXProject "LearnEnglish" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CF43E871300A7CFB00CB9AE3 /* Debug */,
|
||||
CF43E872300A7CFB00CB9AE3 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CF43E873300A7CFB00CB9AE3 /* Build configuration list for PBXNativeTarget "LearnEnglish" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CF43E874300A7CFB00CB9AE3 /* Debug */,
|
||||
CF43E875300A7CFB00CB9AE3 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CF43E860300A7CFA00CB9AE3 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"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" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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,17 @@
|
||||
//
|
||||
// LearnEnglishApp.swift
|
||||
// LearnEnglish
|
||||
//
|
||||
// Created by fish on 2026/7/17.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct LearnEnglishApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
enum AppRoute: Hashable {
|
||||
case category(WordCategory)
|
||||
case word(Word)
|
||||
case quizSetup(QuizSetupRequest)
|
||||
case quiz(QuizConfig)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
|
||||
enum QuizMode: String, Hashable {
|
||||
case enToZh = "英选汉"
|
||||
case zhToEn = "汉选英"
|
||||
}
|
||||
|
||||
struct QuizSetupRequest: Hashable {
|
||||
let mode: QuizMode
|
||||
let words: [Word]
|
||||
}
|
||||
|
||||
struct QuizConfig: Hashable {
|
||||
let mode: QuizMode
|
||||
let words: [Word]
|
||||
}
|
||||
|
||||
struct QuizQuestion: Identifiable {
|
||||
var id: String { word.id }
|
||||
let word: Word
|
||||
let prompt: String
|
||||
let options: [String]
|
||||
let answer: String
|
||||
}
|
||||
|
||||
enum QuizGenerator {
|
||||
static func makeQuestions(mode: QuizMode, words: [Word]) -> [QuizQuestion] {
|
||||
words.shuffled().map { word in
|
||||
let distractors = words.filter { $0 != word }.shuffled().prefix(3)
|
||||
let options = (distractors.map { optionText($0, mode) } + [optionText(word, mode)]).shuffled()
|
||||
return QuizQuestion(
|
||||
word: word,
|
||||
prompt: promptText(word, mode),
|
||||
options: options,
|
||||
answer: optionText(word, mode)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func promptText(_ word: Word, _ mode: QuizMode) -> String {
|
||||
mode == .enToZh ? word.word : word.meaning
|
||||
}
|
||||
|
||||
private static func optionText(_ word: Word, _ mode: QuizMode) -> String {
|
||||
mode == .enToZh ? word.meaning : word.word
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
struct WordBank: Codable {
|
||||
let version: Int
|
||||
let categories: [WordCategory]
|
||||
}
|
||||
|
||||
struct WordCategory: Codable, Identifiable, Hashable {
|
||||
let id: String
|
||||
let name: String
|
||||
let icon: String
|
||||
let words: [Word]
|
||||
}
|
||||
|
||||
struct Word: Codable, Identifiable, Hashable {
|
||||
var id: String { word }
|
||||
let word: String
|
||||
let phonetic: String
|
||||
let meaning: String
|
||||
let example: String
|
||||
let exampleMeaning: String
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"version": 1,
|
||||
"categories": [
|
||||
{
|
||||
"id": "colors",
|
||||
"name": "颜色",
|
||||
"icon": "paintpalette.fill",
|
||||
"words": [
|
||||
{ "word": "red", "phonetic": "/red/", "meaning": "红色", "example": "The apple is red.", "exampleMeaning": "这个苹果是红色的。" },
|
||||
{ "word": "blue", "phonetic": "/bluː/", "meaning": "蓝色", "example": "The sky is blue.", "exampleMeaning": "天空是蓝色的。" },
|
||||
{ "word": "green", "phonetic": "/ɡriːn/", "meaning": "绿色", "example": "The grass is green.", "exampleMeaning": "草地是绿色的。" },
|
||||
{ "word": "yellow", "phonetic": "/ˈjeloʊ/", "meaning": "黄色", "example": "The banana is yellow.", "exampleMeaning": "香蕉是黄色的。" },
|
||||
{ "word": "black", "phonetic": "/blæk/", "meaning": "黑色", "example": "The cat is black.", "exampleMeaning": "这只猫是黑色的。" },
|
||||
{ "word": "white", "phonetic": "/waɪt/", "meaning": "白色", "example": "The snow is white.", "exampleMeaning": "雪是白色的。" },
|
||||
{ "word": "orange", "phonetic": "/ˈɔːrɪndʒ/", "meaning": "橙色", "example": "The orange is orange.", "exampleMeaning": "橙子是橙色的。" },
|
||||
{ "word": "purple", "phonetic": "/ˈpɜːrpl/", "meaning": "紫色", "example": "The grapes are purple.", "exampleMeaning": "葡萄是紫色的。" },
|
||||
{ "word": "pink", "phonetic": "/pɪŋk/", "meaning": "粉色", "example": "The flower is pink.", "exampleMeaning": "这朵花是粉色的。" },
|
||||
{ "word": "brown", "phonetic": "/braʊn/", "meaning": "棕色", "example": "The bear is brown.", "exampleMeaning": "这只熊是棕色的。" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "animals",
|
||||
"name": "动物",
|
||||
"icon": "pawprint.fill",
|
||||
"words": [
|
||||
{ "word": "cat", "phonetic": "/kæt/", "meaning": "猫", "example": "The cat is cute.", "exampleMeaning": "这只猫很可爱。" },
|
||||
{ "word": "dog", "phonetic": "/dɔːɡ/", "meaning": "狗", "example": "The dog is running.", "exampleMeaning": "狗在跑。" },
|
||||
{ "word": "bird", "phonetic": "/bɜːrd/", "meaning": "鸟", "example": "The bird can fly.", "exampleMeaning": "鸟会飞。" },
|
||||
{ "word": "fish", "phonetic": "/fɪʃ/", "meaning": "鱼", "example": "The fish swims fast.", "exampleMeaning": "鱼游得很快。" },
|
||||
{ "word": "rabbit", "phonetic": "/ˈræbɪt/", "meaning": "兔子", "example": "The rabbit likes carrots.", "exampleMeaning": "兔子喜欢胡萝卜。" },
|
||||
{ "word": "tiger", "phonetic": "/ˈtaɪɡər/", "meaning": "老虎", "example": "The tiger is strong.", "exampleMeaning": "老虎很强壮。" },
|
||||
{ "word": "elephant", "phonetic": "/ˈelɪfənt/", "meaning": "大象", "example": "The elephant has a long nose.", "exampleMeaning": "大象有长长的鼻子。" },
|
||||
{ "word": "monkey", "phonetic": "/ˈmʌŋki/", "meaning": "猴子", "example": "The monkey eats bananas.", "exampleMeaning": "猴子吃香蕉。" },
|
||||
{ "word": "panda", "phonetic": "/ˈpændə/", "meaning": "熊猫", "example": "The panda is black and white.", "exampleMeaning": "熊猫是黑白相间的。" },
|
||||
{ "word": "lion", "phonetic": "/ˈlaɪən/", "meaning": "狮子", "example": "The lion is the king of animals.", "exampleMeaning": "狮子是动物之王。" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fruits",
|
||||
"name": "水果",
|
||||
"icon": "leaf.fill",
|
||||
"words": [
|
||||
{ "word": "apple", "phonetic": "/ˈæpl/", "meaning": "苹果", "example": "I eat an apple every day.", "exampleMeaning": "我每天吃一个苹果。" },
|
||||
{ "word": "banana", "phonetic": "/bəˈnænə/", "meaning": "香蕉", "example": "Monkeys like bananas.", "exampleMeaning": "猴子喜欢香蕉。" },
|
||||
{ "word": "orange", "phonetic": "/ˈɔːrɪndʒ/", "meaning": "橙子", "example": "The orange is sweet.", "exampleMeaning": "这个橙子很甜。" },
|
||||
{ "word": "grape", "phonetic": "/ɡreɪp/", "meaning": "葡萄", "example": "Grapes can make wine.", "exampleMeaning": "葡萄可以酿酒。" },
|
||||
{ "word": "watermelon", "phonetic": "/ˈwɔːtərmelən/", "meaning": "西瓜", "example": "Watermelon is a summer fruit.", "exampleMeaning": "西瓜是夏天的水果。" },
|
||||
{ "word": "strawberry", "phonetic": "/ˈstrɔːberi/", "meaning": "草莓", "example": "The strawberry is red and sweet.", "exampleMeaning": "草莓又红又甜。" },
|
||||
{ "word": "peach", "phonetic": "/piːtʃ/", "meaning": "桃子", "example": "The peach is juicy.", "exampleMeaning": "桃子多汁。" },
|
||||
{ "word": "pear", "phonetic": "/per/", "meaning": "梨", "example": "The pear is crisp.", "exampleMeaning": "梨很脆。" },
|
||||
{ "word": "mango", "phonetic": "/ˈmæŋɡoʊ/", "meaning": "芒果", "example": "Mangoes grow in hot places.", "exampleMeaning": "芒果生长在炎热的地方。" },
|
||||
{ "word": "cherry", "phonetic": "/ˈtʃeri/", "meaning": "樱桃", "example": "Cherries are small and red.", "exampleMeaning": "樱桃又小又红。" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "numbers",
|
||||
"name": "数字",
|
||||
"icon": "number",
|
||||
"words": [
|
||||
{ "word": "one", "phonetic": "/wʌn/", "meaning": "一", "example": "I have one book.", "exampleMeaning": "我有一本书。" },
|
||||
{ "word": "two", "phonetic": "/tuː/", "meaning": "二", "example": "Two birds are singing.", "exampleMeaning": "两只鸟在唱歌。" },
|
||||
{ "word": "three", "phonetic": "/θriː/", "meaning": "三", "example": "Three cats are sleeping.", "exampleMeaning": "三只猫在睡觉。" },
|
||||
{ "word": "four", "phonetic": "/fɔːr/", "meaning": "四", "example": "The table has four legs.", "exampleMeaning": "桌子有四条腿。" },
|
||||
{ "word": "five", "phonetic": "/faɪv/", "meaning": "五", "example": "I have five fingers.", "exampleMeaning": "我有五根手指。" },
|
||||
{ "word": "six", "phonetic": "/sɪks/", "meaning": "六", "example": "Six ducks are swimming.", "exampleMeaning": "六只鸭子在游泳。" },
|
||||
{ "word": "seven", "phonetic": "/ˈsevn/", "meaning": "七", "example": "A week has seven days.", "exampleMeaning": "一周有七天。" },
|
||||
{ "word": "eight", "phonetic": "/eɪt/", "meaning": "八", "example": "The spider has eight legs.", "exampleMeaning": "蜘蛛有八条腿。" },
|
||||
{ "word": "nine", "phonetic": "/naɪn/", "meaning": "九", "example": "Nine students are in the room.", "exampleMeaning": "房间里有九个学生。" },
|
||||
{ "word": "ten", "phonetic": "/ten/", "meaning": "十", "example": "I can count to ten.", "exampleMeaning": "我能数到十。" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "family",
|
||||
"name": "家庭",
|
||||
"icon": "house.fill",
|
||||
"words": [
|
||||
{ "word": "father", "phonetic": "/ˈfɑːðər/", "meaning": "爸爸", "example": "My father is tall.", "exampleMeaning": "我爸爸很高。" },
|
||||
{ "word": "mother", "phonetic": "/ˈmʌðər/", "meaning": "妈妈", "example": "My mother cooks well.", "exampleMeaning": "我妈妈做饭很好吃。" },
|
||||
{ "word": "brother", "phonetic": "/ˈbrʌðər/", "meaning": "兄弟", "example": "My brother likes soccer.", "exampleMeaning": "我哥哥喜欢足球。" },
|
||||
{ "word": "sister", "phonetic": "/ˈsɪstər/", "meaning": "姐妹", "example": "My sister can draw.", "exampleMeaning": "我妹妹会画画。" },
|
||||
{ "word": "grandfather", "phonetic": "/ˈɡrænfɑːðər/", "meaning": "爷爷/外公", "example": "My grandfather tells stories.", "exampleMeaning": "我爷爷会讲故事。" },
|
||||
{ "word": "grandmother", "phonetic": "/ˈɡrænmʌðər/", "meaning": "奶奶/外婆", "example": "My grandmother makes dumplings.", "exampleMeaning": "我奶奶会包饺子。" },
|
||||
{ "word": "uncle", "phonetic": "/ˈʌŋkl/", "meaning": "叔叔/舅舅", "example": "My uncle lives in Beijing.", "exampleMeaning": "我叔叔住在北京。" },
|
||||
{ "word": "aunt", "phonetic": "/ænt/", "meaning": "阿姨/姑姑", "example": "My aunt gives me gifts.", "exampleMeaning": "我阿姨送我礼物。" },
|
||||
{ "word": "cousin", "phonetic": "/ˈkʌzn/", "meaning": "堂/表兄弟姐妹", "example": "My cousin and I play together.", "exampleMeaning": "我和我表哥一起玩。" },
|
||||
{ "word": "baby", "phonetic": "/ˈbeɪbi/", "meaning": "婴儿", "example": "The baby is sleeping.", "exampleMeaning": "宝宝在睡觉。" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import AVFoundation
|
||||
|
||||
@Observable
|
||||
final class SpeechService: NSObject {
|
||||
private let synthesizer = AVSpeechSynthesizer()
|
||||
private(set) var speakingText: String?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
synthesizer.delegate = self
|
||||
}
|
||||
|
||||
func speak(_ text: String) {
|
||||
if synthesizer.isSpeaking {
|
||||
synthesizer.stopSpeaking(at: .immediate)
|
||||
}
|
||||
let utterance = AVSpeechUtterance(string: text)
|
||||
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
|
||||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate * 0.9
|
||||
speakingText = text
|
||||
synthesizer.speak(utterance)
|
||||
}
|
||||
}
|
||||
|
||||
extension SpeechService: AVSpeechSynthesizerDelegate {
|
||||
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
|
||||
speakingText = nil
|
||||
}
|
||||
|
||||
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
|
||||
speakingText = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
enum WordStoreError: Error, LocalizedError {
|
||||
case fileNotFound
|
||||
case decodingFailed(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .fileNotFound: return "词库文件缺失"
|
||||
case .decodingFailed(let error): return "词库解析失败:\(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Observable
|
||||
final class WordStore {
|
||||
private(set) var bank: WordBank?
|
||||
private(set) var loadError: String?
|
||||
|
||||
var categories: [WordCategory] { bank?.categories ?? [] }
|
||||
|
||||
func load() {
|
||||
guard bank == nil, loadError == nil else { return }
|
||||
guard let url = Bundle.main.url(forResource: "words", withExtension: "json") else {
|
||||
loadError = WordStoreError.fileNotFound.localizedDescription
|
||||
return
|
||||
}
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
bank = try JSONDecoder().decode(WordBank.self, from: data)
|
||||
} catch {
|
||||
loadError = WordStoreError.decodingFailed(error).localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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,70 @@
|
||||
import SwiftUI
|
||||
|
||||
struct QuizResultView: View {
|
||||
let total: Int
|
||||
let wrongWords: [Word]
|
||||
let duration: TimeInterval
|
||||
let onRestart: () -> Void
|
||||
let onExit: () -> Void
|
||||
|
||||
private var correctCount: Int { total - wrongWords.count }
|
||||
|
||||
private var durationText: String {
|
||||
let seconds = Int(duration)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("再来一轮", action: onRestart)
|
||||
.frame(maxWidth: .infinity)
|
||||
Button("返回", action: onExit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
QuizResultView(
|
||||
total: 10,
|
||||
wrongWords: [
|
||||
Word(word: "red", phonetic: "/red/", meaning: "红色", example: "The apple is red.", exampleMeaning: "苹果是红色的。")
|
||||
],
|
||||
duration: 75,
|
||||
onRestart: {},
|
||||
onExit: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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,144 @@
|
||||
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 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 {
|
||||
Button {
|
||||
speech.speak(question.prompt)
|
||||
} label: {
|
||||
Image(systemName: "speaker.wave.2.fill")
|
||||
.font(.title2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
ForEach(question.options, id: \.self) { option in
|
||||
Button {
|
||||
select(option, for: question)
|
||||
} label: {
|
||||
Text(option)
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(optionColor(option, for: question))
|
||||
.disabled(selectedOption != nil)
|
||||
}
|
||||
}
|
||||
|
||||
if let selectedOption {
|
||||
VStack(spacing: 12) {
|
||||
Text(selectedOption == question.answer ? "回答正确" : "正确答案:\(question.answer)")
|
||||
.font(.headline)
|
||||
.foregroundStyle(selectedOption == question.answer ? .green : .red)
|
||||
Button(currentIndex + 1 < questions.count ? "下一题" : "查看成绩") {
|
||||
next()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
finished = true
|
||||
}
|
||||
}
|
||||
|
||||
private func restart() {
|
||||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||
currentIndex = 0
|
||||
selectedOption = nil
|
||||
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,81 @@
|
||||
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,50 @@
|
||||
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) }
|
||||
} 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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user