From a4d86d21b047489189762b62d9190a8f5bb4f9ac Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 8 Jul 2026 14:58:13 +0900 Subject: [PATCH 1/5] Add SwiftUI growing text editor demo --- Demo/Base.lproj/LaunchScreen.xib | 41 -- .../AppIcon.appiconset/Contents.json | 38 - .../Development.xcodeproj/project.pbxproj | 337 +++++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../Development/Assets.xcassets/Contents.json | 6 + Development/Development/ContentView.swift | 24 + Development/Development/DevelopmentApp.swift | 17 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 + .../Assets.xcassets/Contents.json | 6 + GrowingTextViewDemo/ContentView.swift | 12 + .../GrowingTextViewDemoApp.swift | 18 + .../SwiftUIGrowingTextEditorPreviews.swift | 654 ++++++++++++++++++ .../UIKitNextGrowingTextViewPreviews.swift | 283 ++++++++ NextGrowingTextView-Demo/AppDelegate.swift | 22 - .../AppIcon.appiconset/Contents.json | 53 -- NextGrowingTextView-Demo/Book.swift | 65 -- .../FixedWidthViewController.swift | 58 -- ...FlexibleWidthCenteringViewController.swift | 59 -- .../Launch Screen.storyboard | 48 -- .../RootContainerViewController.swift | 25 - .../UIControl+Closure.swift | 67 -- NextGrowingTextView.podspec | 16 - NextGrowingTextView.xcodeproj/project.pbxproj | 483 ++++--------- .../xcshareddata/swiftpm/Package.resolved | 151 ++-- .../GrowingTextEditor.swift | 292 ++++++++ Package.swift | 14 +- README.md | 37 +- 30 files changed, 1986 insertions(+), 939 deletions(-) delete mode 100644 Demo/Base.lproj/LaunchScreen.xib delete mode 100644 Demo/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Development/Development.xcodeproj/project.pbxproj create mode 100644 Development/Development.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Development/Development/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Development/Development/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Development/Development/Assets.xcassets/Contents.json create mode 100644 Development/Development/ContentView.swift create mode 100644 Development/Development/DevelopmentApp.swift create mode 100644 GrowingTextViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 GrowingTextViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 GrowingTextViewDemo/Assets.xcassets/Contents.json create mode 100644 GrowingTextViewDemo/ContentView.swift create mode 100644 GrowingTextViewDemo/GrowingTextViewDemoApp.swift create mode 100644 GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift create mode 100644 GrowingTextViewDemo/UIKitNextGrowingTextViewPreviews.swift delete mode 100644 NextGrowingTextView-Demo/AppDelegate.swift delete mode 100644 NextGrowingTextView-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 NextGrowingTextView-Demo/Book.swift delete mode 100644 NextGrowingTextView-Demo/FixedWidthViewController.swift delete mode 100644 NextGrowingTextView-Demo/FlexibleWidthCenteringViewController.swift delete mode 100644 NextGrowingTextView-Demo/Launch Screen.storyboard delete mode 100644 NextGrowingTextView-Demo/RootContainerViewController.swift delete mode 100644 NextGrowingTextView-Demo/UIControl+Closure.swift delete mode 100644 NextGrowingTextView.podspec create mode 100644 NextGrowingTextViewSwiftUI/GrowingTextEditor.swift diff --git a/Demo/Base.lproj/LaunchScreen.xib b/Demo/Base.lproj/LaunchScreen.xib deleted file mode 100644 index 2f23c32..0000000 --- a/Demo/Base.lproj/LaunchScreen.xib +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Demo/Images.xcassets/AppIcon.appiconset/Contents.json b/Demo/Images.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d3942e9..0000000 --- a/Demo/Images.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/Development/Development.xcodeproj/project.pbxproj b/Development/Development.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5f89132 --- /dev/null +++ b/Development/Development.xcodeproj/project.pbxproj @@ -0,0 +1,337 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + B01DEB132FFE1180009452C9 /* Development.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Development.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B01DEB152FFE1180009452C9 /* Development */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Development; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + B01DEB102FFE1180009452C9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B01DEB0A2FFE1180009452C9 = { + isa = PBXGroup; + children = ( + B01DEB152FFE1180009452C9 /* Development */, + B01DEB142FFE1180009452C9 /* Products */, + ); + sourceTree = ""; + }; + B01DEB142FFE1180009452C9 /* Products */ = { + isa = PBXGroup; + children = ( + B01DEB132FFE1180009452C9 /* Development.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B01DEB122FFE1180009452C9 /* Development */ = { + isa = PBXNativeTarget; + buildConfigurationList = B01DEB1E2FFE1181009452C9 /* Build configuration list for PBXNativeTarget "Development" */; + buildPhases = ( + B01DEB0F2FFE1180009452C9 /* Sources */, + B01DEB102FFE1180009452C9 /* Frameworks */, + B01DEB112FFE1180009452C9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + B01DEB152FFE1180009452C9 /* Development */, + ); + name = Development; + packageProductDependencies = ( + ); + productName = Development; + productReference = B01DEB132FFE1180009452C9 /* Development.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B01DEB0B2FFE1180009452C9 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2660; + LastUpgradeCheck = 2660; + TargetAttributes = { + B01DEB122FFE1180009452C9 = { + CreatedOnToolsVersion = 26.6; + }; + }; + }; + buildConfigurationList = B01DEB0E2FFE1180009452C9 /* Build configuration list for PBXProject "Development" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B01DEB0A2FFE1180009452C9; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = B01DEB142FFE1180009452C9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B01DEB122FFE1180009452C9 /* Development */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B01DEB112FFE1180009452C9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B01DEB0F2FFE1180009452C9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + B01DEB1C2FFE1181009452C9 /* 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 = KU2QEJ9K3Z; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + 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; + }; + B01DEB1D2FFE1181009452C9 /* 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 = KU2QEJ9K3Z; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B01DEB1F2FFE1181009452C9 /* 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 = KU2QEJ9K3Z; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.muukii.Development; + 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; + }; + B01DEB202FFE1181009452C9 /* 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 = KU2QEJ9K3Z; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.muukii.Development; + 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; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B01DEB0E2FFE1180009452C9 /* Build configuration list for PBXProject "Development" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B01DEB1C2FFE1181009452C9 /* Debug */, + B01DEB1D2FFE1181009452C9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B01DEB1E2FFE1181009452C9 /* Build configuration list for PBXNativeTarget "Development" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B01DEB1F2FFE1181009452C9 /* Debug */, + B01DEB202FFE1181009452C9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B01DEB0B2FFE1180009452C9 /* Project object */; +} diff --git a/Development/Development.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Development/Development.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Development/Development.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Development/Development/Assets.xcassets/AccentColor.colorset/Contents.json b/Development/Development/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Development/Development/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Development/Development/Assets.xcassets/AppIcon.appiconset/Contents.json b/Development/Development/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Development/Development/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "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" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Development/Development/Assets.xcassets/Contents.json b/Development/Development/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Development/Development/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Development/Development/ContentView.swift b/Development/Development/ContentView.swift new file mode 100644 index 0000000..ec43de2 --- /dev/null +++ b/Development/Development/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// Development +// +// Created by Hiroshi Kimura on 2026/07/08. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Development/Development/DevelopmentApp.swift b/Development/Development/DevelopmentApp.swift new file mode 100644 index 0000000..1a20f7b --- /dev/null +++ b/Development/Development/DevelopmentApp.swift @@ -0,0 +1,17 @@ +// +// DevelopmentApp.swift +// Development +// +// Created by Hiroshi Kimura on 2026/07/08. +// + +import SwiftUI + +@main +struct DevelopmentApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/GrowingTextViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/GrowingTextViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/GrowingTextViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GrowingTextViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/GrowingTextViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/GrowingTextViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "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" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GrowingTextViewDemo/Assets.xcassets/Contents.json b/GrowingTextViewDemo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/GrowingTextViewDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GrowingTextViewDemo/ContentView.swift b/GrowingTextViewDemo/ContentView.swift new file mode 100644 index 0000000..2f10ab2 --- /dev/null +++ b/GrowingTextViewDemo/ContentView.swift @@ -0,0 +1,12 @@ +import SwiftUI +import StorybookKit + +/// Root view for the demo application. +/// +/// `Storybook` discovers `#Preview` declarations in this target at runtime, so +/// each demo remains usable both from Xcode Previews and from the app itself. +struct ContentView: View { + var body: some View { + Storybook() + } +} diff --git a/GrowingTextViewDemo/GrowingTextViewDemoApp.swift b/GrowingTextViewDemo/GrowingTextViewDemoApp.swift new file mode 100644 index 0000000..cfd04fa --- /dev/null +++ b/GrowingTextViewDemo/GrowingTextViewDemoApp.swift @@ -0,0 +1,18 @@ +// +// GrowingTextViewDemoApp.swift +// GrowingTextViewDemo +// +// Created by Hiroshi Kimura on 2026/07/08. +// Copyright © 2026 muukii. All rights reserved. +// + +import SwiftUI + +@main +struct GrowingTextViewDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift b/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift new file mode 100644 index 0000000..293d451 --- /dev/null +++ b/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift @@ -0,0 +1,654 @@ +import NextGrowingTextViewSwiftUI +import SwiftUI + +/// A full-screen catalog that shows the main SwiftUI growing editor behaviors +/// in one scrollable surface. +struct SwiftUIGrowingTextEditorOverviewPreview: View { + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + SwiftUIGrowingTextEditorPreviewHeader() + SwiftUIGrowingTextEditorReadmeSection() + SwiftUIGrowingTextEditorMessagesShowcaseSection() + SwiftUIGrowingTextEditorSizingSection() + SwiftUIGrowingTextEditorComposerSection() + } + .padding(20) + } + .background(Color(.systemGroupedBackground)) + } +} + +/// Header copy for the SwiftUI preview catalog. +private struct SwiftUIGrowingTextEditorPreviewHeader: View { + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("SwiftUI") + .font(.largeTitle.bold()) + + Text("GrowingTextEditor previews built from the SwiftUI package product.") + .font(.body) + .foregroundStyle(.secondary) + } + } +} + +/// A README-shaped example that keeps the public API sample easy to verify. +private struct SwiftUIGrowingTextEditorReadmeSection: View { + + @State private var text = """ + This editor starts at one line, grows as content wraps, and scrolls after six lines. + """ + + var body: some View { + GrowingTextEditorDemoCard( + title: "README configuration", + description: "Matches the SwiftUI usage snippet: min 1, max 6, custom padding, and line spacing." + ) { + GrowingTextEditor( + text: $text, + placeholder: "Write something...", + configuration: GrowingTextEditorConfiguration( + minLines: 1, + maxLines: 6, + horizontalPadding: 8, + verticalPadding: 4, + lineSpacing: 3 + ), + font: .body + ) + .background(.yellow.opacity(0.18), in: RoundedRectangle(cornerRadius: 12)) + } + } +} + +/// A polished, Messages-inspired preview that shows the editor inside a chat +/// composer instead of a neutral component card. +private struct SwiftUIGrowingTextEditorMessagesShowcaseSection: View { + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 4) { + Text("Messages-style composer") + .font(.title2.bold()) + + Text("A richer preview for seeing the growing editor in a familiar chat surface.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + + SwiftUIGrowingTextEditorMessagesPreview() + .frame(height: 640) + } + } +} + +/// A Messages-inspired screen that highlights `GrowingTextEditor` in context. +struct SwiftUIGrowingTextEditorMessagesPreview: View { + + @State private var draft = """ + This composer uses GrowingTextEditor, so the input expands as the message gets longer. + """ + + var body: some View { + ZStack { + SwiftUIGrowingTextEditorMessagesBackdrop() + + VStack(spacing: 0) { + SwiftUIGrowingTextEditorMessagesHeader( + contactName: "Aoi", + status: "iMessage" + ) + + SwiftUIGrowingTextEditorMessagesTranscript( + messages: SwiftUIGrowingTextEditorMessage.samples + ) + + SwiftUIGrowingTextEditorMessagesComposer(text: $draft) + } + .background(.regularMaterial) + } + .background(Color(.systemGroupedBackground)) + } +} + +/// Atmospheric background used behind the Messages-style phone surface. +private struct SwiftUIGrowingTextEditorMessagesBackdrop: View { + + var body: some View { + LinearGradient( + colors: [ + Color(red: 0.78, green: 0.90, blue: 1.00), + Color(red: 0.95, green: 0.97, blue: 1.00), + Color(red: 0.95, green: 0.92, blue: 0.86), + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .overlay(alignment: .topTrailing) { + Circle() + .fill(.blue.opacity(0.16)) + .frame(width: 220, height: 220) + .blur(radius: 18) + .offset(x: 80, y: -80) + } + .overlay(alignment: .bottomLeading) { + Circle() + .fill(.mint.opacity(0.18)) + .frame(width: 180, height: 180) + .blur(radius: 20) + .offset(x: -70, y: 70) + } + } +} + +/// Header chrome for the Messages-style preview. +private struct SwiftUIGrowingTextEditorMessagesHeader: View { + + let contactName: String + let status: String + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "chevron.left") + .font(.headline.weight(.semibold)) + .foregroundStyle(.blue) + + Spacer() + + VStack(spacing: 5) { + ZStack { + Circle() + .fill( + LinearGradient( + colors: [.cyan, .blue], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + + Text(String(contactName.prefix(1))) + .font(.headline.weight(.semibold)) + .foregroundStyle(.white) + } + .frame(width: 42, height: 42) + + VStack(spacing: 1) { + Text(contactName) + .font(.caption.weight(.semibold)) + + Text(status) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "video") + .font(.headline.weight(.semibold)) + .foregroundStyle(.blue) + } + .padding(.horizontal, 16) + .padding(.top, 14) + .padding(.bottom, 10) + .background(.thinMaterial) + } +} + +/// Scrollable transcript for the Messages-style preview. +private struct SwiftUIGrowingTextEditorMessagesTranscript: View { + + let messages: [SwiftUIGrowingTextEditorMessage] + + var body: some View { + ScrollView { + VStack(spacing: 10) { + Text("Today 14:32") + .font(.caption2.weight(.medium)) + .foregroundStyle(.secondary) + .padding(.vertical, 8) + + ForEach(messages) { message in + SwiftUIGrowingTextEditorMessageBubble(message: message) + } + } + .padding(.horizontal, 14) + .padding(.bottom, 18) + } + .scrollIndicators(.hidden) + } +} + +/// One transcript bubble in the Messages-style preview. +private struct SwiftUIGrowingTextEditorMessageBubble: View { + + let message: SwiftUIGrowingTextEditorMessage + + var body: some View { + HStack(alignment: .bottom, spacing: 6) { + if message.isOutgoing { + Spacer(minLength: 42) + } + + Text(message.text) + .font(.body) + .foregroundStyle(message.isOutgoing ? .white : .primary) + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(messageBackground, in: bubbleShape) + .shadow(color: .black.opacity(message.isOutgoing ? 0.14 : 0.06), radius: 4, x: 0, y: 2) + + if message.isOutgoing == false { + Spacer(minLength: 42) + } + } + .frame(maxWidth: .infinity, alignment: message.isOutgoing ? .trailing : .leading) + } + + private var messageBackground: some ShapeStyle { + if message.isOutgoing { + return AnyShapeStyle( + LinearGradient( + colors: [.blue, Color(red: 0.10, green: 0.45, blue: 1.00)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + } else { + return AnyShapeStyle(.white.opacity(0.92)) + } + } + + private var bubbleShape: UnevenRoundedRectangle { + if message.isOutgoing { + UnevenRoundedRectangle( + topLeadingRadius: 20, + bottomLeadingRadius: 20, + bottomTrailingRadius: 6, + topTrailingRadius: 20, + style: .continuous + ) + } else { + UnevenRoundedRectangle( + topLeadingRadius: 20, + bottomLeadingRadius: 6, + bottomTrailingRadius: 20, + topTrailingRadius: 20, + style: .continuous + ) + } + } +} + +/// Composer bar for the Messages-style preview. +private struct SwiftUIGrowingTextEditorMessagesComposer: View { + + @Binding var text: String + + var body: some View { + HStack(alignment: .bottom, spacing: 9) { + Button { + text = SwiftUIGrowingTextEditorMessagesText.cameraMessage + } label: { + Image(systemName: "camera.fill") + .font(.headline) + .foregroundStyle(.secondary) + .frame(width: 34, height: 34) + .background(.thinMaterial, in: Circle()) + } + .accessibilityLabel("Insert camera message") + + GrowingTextEditor( + text: $text, + placeholder: "iMessage", + configuration: GrowingTextEditorConfiguration( + minLines: 1, + maxLines: 5, + horizontalPadding: 9, + verticalPadding: 5, + lineSpacing: 2 + ), + font: .body + ) + .background(.white, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .stroke(.black.opacity(0.08), lineWidth: 1) + } + + Button { + text = "" + } label: { + Image(systemName: "arrow.up.circle.fill") + .font(.system(size: 32, weight: .semibold)) + .foregroundStyle(text.isEmpty ? Color.secondary.opacity(0.45) : Color.blue) + } + .accessibilityLabel("Send") + .disabled(text.isEmpty) + } + .padding(.horizontal, 12) + .padding(.top, 10) + .padding(.bottom, 14) + .background(.ultraThinMaterial) + } +} + +/// Message data rendered by the Messages-style preview. +private struct SwiftUIGrowingTextEditorMessage: Identifiable, Equatable { + + let id: Int + let text: String + let isOutgoing: Bool + + static let samples: [SwiftUIGrowingTextEditorMessage] = [ + .init(id: 1, text: "Could this fit into a Messages-style composer?", isOutgoing: false), + .init(id: 2, text: "Yes. The editor grows without the surrounding bar jumping around.", isOutgoing: true), + .init(id: 3, text: "Nice. It should still feel light when the text wraps.", isOutgoing: false), + .init(id: 4, text: "Try editing the draft below and watch it expand up to five lines.", isOutgoing: true), + ] +} + +/// Shared copy used by the Messages-style preview controls. +private enum SwiftUIGrowingTextEditorMessagesText { + + static let cameraMessage = "Photo attached. Adding a little more text to show the growing composer wrapping across multiple lines." +} + +/// A pair of SwiftUI sizing previews that mirror the old demo's width cases. +private struct SwiftUIGrowingTextEditorSizingSection: View { + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Sizing") + .font(.title2.bold()) + + SwiftUIGrowingTextEditorSizingPreview(layout: .flexibleWidth) + SwiftUIGrowingTextEditorSizingPreview(layout: .fixedWidth) + } + } +} + +/// Width behavior for SwiftUI growing editor previews. +private enum SwiftUIGrowingTextEditorPreviewLayout { + + /// The editor keeps a narrow ideal width and expands as the text needs more + /// horizontal space. + case flexibleWidth + + /// The editor fills the available horizontal space before growing vertically. + case fixedWidth + + var title: LocalizedStringResource { + switch self { + case .flexibleWidth: + "Flexible width" + case .fixedWidth: + "Fixed width" + } + } + + var description: LocalizedStringResource { + switch self { + case .flexibleWidth: + "Starts compact and centered so wrapped text drives vertical growth." + case .fixedWidth: + "Uses the full container width before growing vertically." + } + } +} + +/// An interactive SwiftUI editor preview with configurable line limits. +private struct SwiftUIGrowingTextEditorSizingPreview: View { + + let layout: SwiftUIGrowingTextEditorPreviewLayout + + @State private var text = "" + @State private var minLines = 1 + @State private var maxLines = 10 + @State private var usesLongPlaceholder = false + + var body: some View { + GrowingTextEditorDemoCard( + title: layout.title, + description: layout.description + ) { + VStack(alignment: .leading, spacing: 14) { + editor + GrowingTextEditorControlPanel( + text: $text, + minLines: $minLines, + maxLines: $maxLines, + usesLongPlaceholder: $usesLongPlaceholder + ) + } + } + } + + private var editor: some View { + growingEditor + .font(.system(size: 20, weight: .bold)) + .frame(maxWidth: maxWidth, alignment: .center) + .background(.yellow.opacity(0.2), in: RoundedRectangle(cornerRadius: 14)) + .frame(maxWidth: .infinity, alignment: .center) + } + + private var growingEditor: some View { + GrowingTextEditor( + text: $text, + placeholder: placeholder, + configuration: GrowingTextEditorConfiguration( + minLines: minLines, + maxLines: maxLines, + horizontalPadding: 8, + verticalPadding: 6, + lineSpacing: 2 + ), + font: .system(size: 20, weight: .bold) + ) + } + + private var placeholder: LocalizedStringKey { + if usesLongPlaceholder { + return "Placeholder, Placeholder, Placeholder, " + } else { + return "Placeholder" + } + } + + private var maxWidth: CGFloat? { + switch layout { + case .flexibleWidth: + 220 + case .fixedWidth: + .infinity + } + } +} + +/// Buttons and steppers that mutate a SwiftUI sizing preview. +private struct GrowingTextEditorControlPanel: View { + + @Binding var text: String + @Binding var minLines: Int + @Binding var maxLines: Int + @Binding var usesLongPlaceholder: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + ViewThatFits { + HStack(spacing: 8) { + textButtons + placeholderButton + } + + VStack(alignment: .leading, spacing: 8) { + textButtons + placeholderButton + } + } + + Stepper("Min lines: \(minLines)", value: $minLines, in: 1...5) + Stepper("Max lines: \(maxLines)", value: $maxLines, in: minLines...10) + } + .onChange(of: minLines) { _, newValue in + maxLines = Swift.max(maxLines, newValue) + } + .buttonStyle(.bordered) + } + + private var textButtons: some View { + HStack(spacing: 8) { + Button("Set text") { + text = GrowingTextEditorDemoText.longMessage + } + + Button("Clear text") { + text = "" + } + } + } + + private var placeholderButton: some View { + Button(usesLongPlaceholder ? "Short placeholder" : "Long placeholder") { + usesLongPlaceholder.toggle() + } + } +} + +/// A practical composer-style example that exercises the max-line cap. +private struct SwiftUIGrowingTextEditorComposerSection: View { + + @State private var message = "" + + var body: some View { + GrowingTextEditorDemoCard( + title: "Composer", + description: "A chat-style input that grows until five lines, then lets the editor scroll." + ) { + HStack(alignment: .bottom, spacing: 10) { + GrowingTextEditor( + text: $message, + placeholder: "Message", + configuration: GrowingTextEditorConfiguration( + minLines: 1, + maxLines: 5, + horizontalPadding: 10, + verticalPadding: 6, + lineSpacing: 2 + ), + font: .body + ) + .background(.quaternary, in: RoundedRectangle(cornerRadius: 18)) + + Button { + message = "" + } label: { + Image(systemName: "arrow.up") + .font(.headline) + .foregroundStyle(.white) + .frame(width: 34, height: 34) + .background(.blue, in: Circle()) + } + .accessibilityLabel("Send") + .disabled(message.isEmpty) + } + } + } +} + +/// A reusable visual container for preview cases. +private struct GrowingTextEditorDemoCard: View { + + let title: LocalizedStringResource + let description: LocalizedStringResource + let content: Content + + init( + title: LocalizedStringResource, + description: LocalizedStringResource, + @ViewBuilder content: () -> Content + ) { + self.title = title + self.description = description + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + GrowingTextEditorDemoCardHeader(title: title, description: description) + content + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.background, in: RoundedRectangle(cornerRadius: 20)) + } +} + +/// Title and helper text for a preview card. +private struct GrowingTextEditorDemoCardHeader: View { + + let title: LocalizedStringResource + let description: LocalizedStringResource + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + + Text(description) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } +} + +/// Shared sample strings used by the interactive previews. +private enum GrowingTextEditorDemoText { + + static let longMessage = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere lectus sed arcu facilisis, at bibendum orci interdum. Integer non luctus lorem. Try editing this text and watch the container grow until it reaches the configured maximum line count. + """ +} + +#Preview("SwiftUI / Overview") { + SwiftUIGrowingTextEditorOverviewPreview() +} + +#Preview("SwiftUI / README configuration") { + ScrollView { + SwiftUIGrowingTextEditorReadmeSection() + .padding(20) + } + .background(Color(.systemGroupedBackground)) +} + +#Preview("SwiftUI / Messages style") { + SwiftUIGrowingTextEditorMessagesPreview() +} + +#Preview("SwiftUI / Flexible width") { + ScrollView { + SwiftUIGrowingTextEditorSizingPreview(layout: .flexibleWidth) + .padding(20) + } + .background(Color(.systemGroupedBackground)) +} + +#Preview("SwiftUI / Fixed width") { + ScrollView { + SwiftUIGrowingTextEditorSizingPreview(layout: .fixedWidth) + .padding(20) + } + .background(Color(.systemGroupedBackground)) +} + +#Preview("SwiftUI / Composer") { + ScrollView { + SwiftUIGrowingTextEditorComposerSection() + .padding(20) + } + .background(Color(.systemGroupedBackground)) +} diff --git a/GrowingTextViewDemo/UIKitNextGrowingTextViewPreviews.swift b/GrowingTextViewDemo/UIKitNextGrowingTextViewPreviews.swift new file mode 100644 index 0000000..4f70011 --- /dev/null +++ b/GrowingTextViewDemo/UIKitNextGrowingTextViewPreviews.swift @@ -0,0 +1,283 @@ +import NextGrowingTextView +import UIKit + +/// A UIKit preview controller that hosts `NextGrowingTextView` with small, +/// self-contained controls for the most important behaviors. +final class UIKitGrowingTextViewPreviewController: UIViewController { + + private let layout: UIKitGrowingTextViewPreviewLayout + private let growingTextView = NextGrowingTextView() + private let minLinesStepper = UIStepper() + private let maxLinesStepper = UIStepper() + private let minLinesLabel = UILabel() + private let maxLinesLabel = UILabel() + + private var minLines = 1 { + didSet { + if maxLines < minLines { + maxLines = minLines + } + applyConfiguration() + } + } + + private var maxLines = 10 { + didSet { + applyConfiguration() + } + } + + private var usesLongPlaceholder = false { + didSet { + updatePlaceholder() + } + } + + /// Creates a preview controller for one UIKit width behavior. + init(layout: UIKitGrowingTextViewPreviewLayout) { + self.layout = layout + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemGroupedBackground + configureGrowingTextView() + configureSteppers() + buildLayout() + applyConfiguration() + } + + private func configureGrowingTextView() { + growingTextView.textView.font = .boldSystemFont(ofSize: 20) + growingTextView.textView.textColor = .label + growingTextView.placeholderLabel.font = growingTextView.textView.font + growingTextView.placeholderLabel.textColor = .placeholderText + growingTextView.placeholderLabel.numberOfLines = 0 + updatePlaceholder() + } + + private func configureSteppers() { + minLinesStepper.minimumValue = 1 + minLinesStepper.maximumValue = 5 + minLinesStepper.value = Double(minLines) + minLinesStepper.addAction(UIAction { [weak self] _ in + self?.minLines = Int(self?.minLinesStepper.value ?? 1) + }, for: .valueChanged) + + maxLinesStepper.minimumValue = 1 + maxLinesStepper.maximumValue = 10 + maxLinesStepper.value = Double(maxLines) + maxLinesStepper.addAction(UIAction { [weak self] _ in + self?.maxLines = Int(self?.maxLinesStepper.value ?? 1) + }, for: .valueChanged) + } + + private func buildLayout() { + let rootStack = UIStackView(arrangedSubviews: [ + makeHeader(), + makeEditorContainer(), + makeControlPanel(), + ]) + rootStack.axis = .vertical + rootStack.alignment = .fill + rootStack.spacing = 16 + rootStack.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(rootStack) + + NSLayoutConstraint.activate([ + rootStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + rootStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + rootStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + ]) + } + + private func makeHeader() -> UIView { + let titleLabel = UILabel() + titleLabel.text = "UIKit" + titleLabel.font = .preferredFont(forTextStyle: .largeTitle) + titleLabel.adjustsFontForContentSizeCategory = true + + let subtitleLabel = UILabel() + subtitleLabel.text = layout.subtitle + subtitleLabel.font = .preferredFont(forTextStyle: .body) + subtitleLabel.textColor = .secondaryLabel + subtitleLabel.numberOfLines = 0 + subtitleLabel.adjustsFontForContentSizeCategory = true + + let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) + stack.axis = .vertical + stack.spacing = 8 + return stack + } + + private func makeEditorContainer() -> UIView { + let container = UIView() + container.backgroundColor = UIColor.systemYellow.withAlphaComponent(0.2) + container.layer.cornerRadius = 14 + container.translatesAutoresizingMaskIntoConstraints = false + + growingTextView.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(growingTextView) + + var constraints: [NSLayoutConstraint] = [ + growingTextView.topAnchor.constraint(equalTo: container.topAnchor), + growingTextView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + growingTextView.centerXAnchor.constraint(equalTo: container.centerXAnchor), + ] + + switch layout { + case .flexibleWidth: + constraints.append(growingTextView.widthAnchor.constraint(equalToConstant: 220)) + constraints.append(growingTextView.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor)) + constraints.append(growingTextView.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor)) + case .fixedWidth: + constraints.append(growingTextView.leadingAnchor.constraint(equalTo: container.leadingAnchor)) + constraints.append(growingTextView.trailingAnchor.constraint(equalTo: container.trailingAnchor)) + } + + NSLayoutConstraint.activate(constraints) + return container + } + + private func makeControlPanel() -> UIView { + let textButtonsStack = UIStackView(arrangedSubviews: [ + makeButton(title: "Set text") { [weak self] in + self?.setLongText() + }, + makeButton(title: "Clear text") { [weak self] in + self?.setText("") + }, + ]) + textButtonsStack.axis = .horizontal + textButtonsStack.spacing = 8 + textButtonsStack.distribution = .fillEqually + + let buttonsStack = UIStackView(arrangedSubviews: [ + textButtonsStack, + makeButton(title: "Toggle placeholder") { [weak self] in + self?.usesLongPlaceholder.toggle() + }, + ]) + buttonsStack.axis = .vertical + buttonsStack.spacing = 8 + + let stack = UIStackView(arrangedSubviews: [ + buttonsStack, + makeStepperRow(titleLabel: minLinesLabel, stepper: minLinesStepper), + makeStepperRow(titleLabel: maxLinesLabel, stepper: maxLinesStepper), + ]) + stack.axis = .vertical + stack.spacing = 12 + return stack + } + + private func makeButton( + title: String, + action: @escaping () -> Void + ) -> UIButton { + let button = UIButton(type: .system) + button.setTitle(title, for: .normal) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) + button.addAction(UIAction { _ in action() }, for: .touchUpInside) + return button + } + + private func makeStepperRow( + titleLabel: UILabel, + stepper: UIStepper + ) -> UIView { + titleLabel.font = .preferredFont(forTextStyle: .body) + titleLabel.adjustsFontForContentSizeCategory = true + + let row = UIStackView(arrangedSubviews: [titleLabel, stepper]) + row.axis = .horizontal + row.alignment = .center + row.spacing = 12 + return row + } + + private func applyConfiguration() { + growingTextView.configuration = .init( + placeholderHorizontalLayout: layout.placeholderHorizontalLayout, + minLines: minLines, + maxLines: maxLines, + isAutomaticScrollToBottomEnabled: true, + isFlashScrollIndicatorsEnabled: true + ) + + minLinesStepper.value = Double(minLines) + maxLinesStepper.minimumValue = Double(minLines) + maxLinesStepper.value = Double(maxLines) + minLinesLabel.text = "Min lines: \(minLines)" + maxLinesLabel.text = "Max lines: \(maxLines)" + } + + private func updatePlaceholder() { + if usesLongPlaceholder { + growingTextView.placeholderLabel.text = "Placeholder, Placeholder, Placeholder, " + } else { + growingTextView.placeholderLabel.text = "Placeholder" + } + } + + private func setLongText() { + setText(UIKitGrowingTextViewPreviewText.longMessage) + } + + private func setText(_ text: String) { + growingTextView.textView.text = text + growingTextView.textView.delegate?.textViewDidChange?(growingTextView.textView) + } +} + +/// Width behavior for UIKit `NextGrowingTextView` previews. +enum UIKitGrowingTextViewPreviewLayout { + + /// The editor is centered with a fixed preview width. + case flexibleWidth + + /// The editor fills the preview container horizontally. + case fixedWidth + + var subtitle: String { + switch self { + case .flexibleWidth: + "NextGrowingTextView preview: flexible width" + case .fixedWidth: + "NextGrowingTextView preview: fixed width" + } + } + + var placeholderHorizontalLayout: NextGrowingTextView.Configuration.PlaceholderHorizontalLayout { + switch self { + case .flexibleWidth: + .leading + case .fixedWidth: + .center + } + } +} + +/// Shared sample strings used by the UIKit previews. +private enum UIKitGrowingTextViewPreviewText { + + static let longMessage = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere lectus sed arcu facilisis, at bibendum orci interdum. Integer non luctus lorem. Try editing this text and watch the container grow until it reaches the configured maximum line count. + """ +} + +#Preview("UIKit / Flexible width") { + UIKitGrowingTextViewPreviewController(layout: .flexibleWidth) +} + +#Preview("UIKit / Fixed width") { + UIKitGrowingTextViewPreviewController(layout: .fixedWidth) +} diff --git a/NextGrowingTextView-Demo/AppDelegate.swift b/NextGrowingTextView-Demo/AppDelegate.swift deleted file mode 100644 index 0e6eebd..0000000 --- a/NextGrowingTextView-Demo/AppDelegate.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit - -import MondrianLayout -import CompositionKit -import NextGrowingTextView -import TypedTextAttributes - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - let newWindow = UIWindow() - newWindow.rootViewController = RootContainerViewController() - newWindow.makeKeyAndVisible() - self.window = newWindow - return true - } - -} diff --git a/NextGrowingTextView-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/NextGrowingTextView-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 8121323..0000000 --- a/NextGrowingTextView-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/NextGrowingTextView-Demo/Book.swift b/NextGrowingTextView-Demo/Book.swift deleted file mode 100644 index 65b823a..0000000 --- a/NextGrowingTextView-Demo/Book.swift +++ /dev/null @@ -1,65 +0,0 @@ - -import StorybookKit -import NextGrowingTextView -import UIKit -import MondrianLayout - -let book = Book(title: "NextGrowingTextView") { - - BookPush(title: "Flexible Width") { - FlexibleWidthCenteringViewController() - } - - BookPush(title: "Fixed Width") { - FixedWidthViewController() - } - -} - - -func makeControlPanel(for growingTextView: NextGrowingTextView) -> UIView { - - let view = UIView() - - Mondrian.buildSubviews(on: view) { - - VGridBlock(columns: [ - .init(.flexible(), spacing: 10), - .init(.flexible(), spacing: 10), - ]) { - - UIButton.make(title: "Set text") { - growingTextView.textView.text = BookGenerator.loremIpsum(length: 300) - } - - UIButton.make(title: "Clear text") { - growingTextView.textView.text = "" - } - - UIButton.make(title: "Max = 3") { - growingTextView.configuration.maxLines = 3 - } - - UIButton.make(title: "Max = 10") { - growingTextView.configuration.maxLines = 10 - } - - UIButton.make(title: "Min = 1") { - growingTextView.configuration.minLines = 1 - } - - UIButton.make(title: "Min = 5") { - growingTextView.configuration.minLines = 5 - } - - UIButton.make(title: "Long placeholder") { - growingTextView.placeholderLabel.text = "Placeholder, Placeholder, Placeholder, " - growingTextView.placeholderLabel.numberOfLines = 0 - } - } - - } - - return view - -} diff --git a/NextGrowingTextView-Demo/FixedWidthViewController.swift b/NextGrowingTextView-Demo/FixedWidthViewController.swift deleted file mode 100644 index bf0ef3c..0000000 --- a/NextGrowingTextView-Demo/FixedWidthViewController.swift +++ /dev/null @@ -1,58 +0,0 @@ -import CompositionKit -import MondrianLayout -import NextGrowingTextView -import StorybookKit -import TypedTextAttributes -import UIKit -import Wrap - -final class FixedWidthViewController: CodeBasedViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - - let textView = NextGrowingTextView()&>.do { - $0.configuration = .init( - placeholderHorizontalLayout: .center, - minLines: 1, - maxLines: 10, - isAutomaticScrollToBottomEnabled: true, - isFlashScrollIndicatorsEnabled: true - ) - $0.textView.font = UIFont.boldSystemFont(ofSize: 20) - $0.textView.textColor = .label - - $0.placeholderLabel.font = $0.textView.font - $0.placeholderLabel.textColor = .init(white: 0.5, alpha: 0.5) - $0.placeholderLabel.text = "Placeholder" - } - - Mondrian.buildSubviews(on: view) { - ZStackBlock { - - VStackBlock(spacing: 10) { - - textView - .viewBlock - .background( - UIView()&>.do { - $0.backgroundColor = .systemYellow.withAlphaComponent(0.2) - } - ) - .alignSelf(.fill) - - makeControlPanel(for: textView) - } - - .alignSelf(.attach(.horizontal)) - - } - .padding(20) - .container(respectingSafeAreaEdges: .all) - - } - } - -} diff --git a/NextGrowingTextView-Demo/FlexibleWidthCenteringViewController.swift b/NextGrowingTextView-Demo/FlexibleWidthCenteringViewController.swift deleted file mode 100644 index 763a3fc..0000000 --- a/NextGrowingTextView-Demo/FlexibleWidthCenteringViewController.swift +++ /dev/null @@ -1,59 +0,0 @@ -import CompositionKit -import MondrianLayout -import NextGrowingTextView -import TypedTextAttributes -import UIKit -import Wrap -import StorybookKit - -final class FlexibleWidthCenteringViewController: CodeBasedViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - - let textView = NextGrowingTextView()&>.do { - $0.configuration = .init( - minLines: 1, - maxLines: 10, - isAutomaticScrollToBottomEnabled: true, - isFlashScrollIndicatorsEnabled: true - ) - $0.textView.font = UIFont.boldSystemFont(ofSize: 20) - $0.textView.textColor = .label - - $0.placeholderLabel.font = $0.textView.font - $0.placeholderLabel.textColor = .init(white: 0.5, alpha: 0.5) - $0.placeholderLabel.text = "Placeholder" - } - - Mondrian.buildSubviews(on: view) { - ZStackBlock { - - VStackBlock(spacing: 10) { - - textView - .viewBlock - .width(.min(30)) - .background( - UIView()&>.do { - $0.backgroundColor = .systemYellow.withAlphaComponent(0.2) - } - ) - .alignSelf(.center) - - makeControlPanel(for: textView) - - } - - .alignSelf(.attach(.horizontal)) - - } - .padding(20) - .container(respectingSafeAreaEdges: .all) - - } - } - -} diff --git a/NextGrowingTextView-Demo/Launch Screen.storyboard b/NextGrowingTextView-Demo/Launch Screen.storyboard deleted file mode 100644 index 3383381..0000000 --- a/NextGrowingTextView-Demo/Launch Screen.storyboard +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/NextGrowingTextView-Demo/RootContainerViewController.swift b/NextGrowingTextView-Demo/RootContainerViewController.swift deleted file mode 100644 index 87e018b..0000000 --- a/NextGrowingTextView-Demo/RootContainerViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -import StorybookKit -import StorybookUI -import UIKit - -final class RootContainerViewController: UIViewController { - - init() { - super.init(nibName: nil, bundle: nil) - - let child = StorybookViewController( - book: book, - dismissHandler: nil - ) - - addChild(child) - view.addSubview(child.view) - child.view.frame = view.bounds - child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/NextGrowingTextView-Demo/UIControl+Closure.swift b/NextGrowingTextView-Demo/UIControl+Closure.swift deleted file mode 100644 index f207cfe..0000000 --- a/NextGrowingTextView-Demo/UIControl+Closure.swift +++ /dev/null @@ -1,67 +0,0 @@ -import UIKit - -private final class Proxy { - static var key: Void? - private weak var base: UIControl? - - init( - _ base: UIControl - ) { - self.base = base - } - - var onTouchUpInside: (() -> Void)? { - didSet { - base?.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside) - } - } - - @objc private func touchUpInside(sender: AnyObject) { - onTouchUpInside?() - } -} - -extension UIControl { - func onTap(_ closure: @escaping () -> Swift.Void) { - tapable.onTouchUpInside = closure - } - - private var tapable: Proxy { - get { - if let handler = objc_getAssociatedObject(self, &Proxy.key) as? Proxy { - return handler - } else { - self.tapable = Proxy(self) - return self.tapable - } - } - set { - objc_setAssociatedObject(self, &Proxy.key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } -} - -extension UIButton { - - static func make( - title: String, - color: UIColor? = nil, - onTap: @escaping () -> Void - ) -> UIButton { - let button = UIButton(type: .system) - button.setAttributedTitle( - NSAttributedString( - string: title, - attributes: ([ - .font: UIFont.preferredFont(forTextStyle: .headline), - .foregroundColor : color - ] as [NSAttributedString.Key : AnyHashable?]) - .compactMapValues { $0 } - ), - for: .normal - ) - button.onTap(onTap) - return button - } - -} diff --git a/NextGrowingTextView.podspec b/NextGrowingTextView.podspec deleted file mode 100644 index cc79380..0000000 --- a/NextGrowingTextView.podspec +++ /dev/null @@ -1,16 +0,0 @@ -Pod::Spec.new do |s| - s.name = "NextGrowingTextView" - s.version = "2.2.0" - s.summary = "The next in the generations of 'growing textviews' optimized for iOS 9 and above." - s.homepage = "https://github.com/muukii/NextGrowingTextView" - s.license = 'MIT' - s.author = { "muukii" => "muukii.app@gmail.com" } - s.source = { :git => "https://github.com/muukii/NextGrowingTextView.git", :tag => s.version.to_s } - s.social_media_url = 'https://twitter.com/muukii_app' - - s.platform = :ios, '9.0' - s.requires_arc = true - s.swift_version = "5.5" - - s.source_files = 'NextGrowingTextView/**/*.swift' -end diff --git a/NextGrowingTextView.xcodeproj/project.pbxproj b/NextGrowingTextView.xcodeproj/project.pbxproj index e0259c3..eb05908 100644 --- a/NextGrowingTextView.xcodeproj/project.pbxproj +++ b/NextGrowingTextView.xcodeproj/project.pbxproj @@ -3,113 +3,37 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ - 4B26B0EC27CF766A00DDBEC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDB87FA1DC0426A00E70D5B /* AppDelegate.swift */; }; - 4B26B0EE27CF766A00DDBEC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4BDB88011DC0426A00E70D5B /* Assets.xcassets */; }; - 4B26B0EF27CF766A00DDBEC0 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BDB880F1DC042F700E70D5B /* Launch Screen.storyboard */; }; - 4B26B0F027CF766A00DDBEC0 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B26B0D027CF60B900DDBEC0 /* Book.swift */; }; - 4B26B0F127CF766A00DDBEC0 /* RootContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B26B0D227CF60E200DDBEC0 /* RootContainerViewController.swift */; }; - 4B26B0F427CF767A00DDBEC0 /* StorybookKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B26B0F327CF767A00DDBEC0 /* StorybookKit */; }; - 4B26B0F627CF767A00DDBEC0 /* StorybookUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4B26B0F527CF767A00DDBEC0 /* StorybookUI */; }; - 4B26B0F727CF767A00DDBEC0 /* NextGrowingTextView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BDB87E51DC0423F00E70D5B /* NextGrowingTextView.framework */; }; - 4B26B0F827CF767A00DDBEC0 /* NextGrowingTextView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BDB87E51DC0423F00E70D5B /* NextGrowingTextView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 4B26B0FD27CF775200DDBEC0 /* FlexibleWidthCenteringViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B26B0FC27CF775200DDBEC0 /* FlexibleWidthCenteringViewController.swift */; }; - 4B26B10227CF77B600DDBEC0 /* MondrianLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 4B26B10127CF77B600DDBEC0 /* MondrianLayout */; }; - 4B26B10727CF782000DDBEC0 /* CompositionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B26B10627CF782000DDBEC0 /* CompositionKit */; }; - 4B26B10C27CF7A6B00DDBEC0 /* TypedTextAttributes in Frameworks */ = {isa = PBXBuildFile; productRef = 4B26B10B27CF7A6B00DDBEC0 /* TypedTextAttributes */; }; - 4B26B11027CF7E9A00DDBEC0 /* Wrap in Frameworks */ = {isa = PBXBuildFile; productRef = 4B26B10F27CF7E9A00DDBEC0 /* Wrap */; }; - 4B26B34A27CF848100DDBEC0 /* SizingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B26B34927CF848100DDBEC0 /* SizingContainerView.swift */; }; - 4B26B34E27CFD12D00DDBEC0 /* UIControl+Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B26B34D27CFD12D00DDBEC0 /* UIControl+Closure.swift */; }; - 4B26B35027CFD31500DDBEC0 /* FixedWidthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B26B34F27CFD31500DDBEC0 /* FixedWidthViewController.swift */; }; - 4BDB87F21DC0425600E70D5B /* InternalTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDB87F01DC0425600E70D5B /* InternalTextView.swift */; }; - 4BDB87F31DC0425600E70D5B /* NextGrowingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDB87F11DC0425600E70D5B /* NextGrowingTextView.swift */; }; + B01DEB372FFE1352009452C9 /* StorybookKit in Frameworks */ = {isa = PBXBuildFile; productRef = B01DEB362FFE1352009452C9 /* StorybookKit */; }; + B01DEB392FFE135A009452C9 /* NextGrowingTextView in Frameworks */ = {isa = PBXBuildFile; productRef = B01DEB382FFE135A009452C9 /* NextGrowingTextView */; }; + B01DEB3B2FFE135A009452C9 /* NextGrowingTextViewSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = B01DEB3A2FFE135A009452C9 /* NextGrowingTextViewSwiftUI */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 4B26B0F927CF767A00DDBEC0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 4BDB87DC1DC0423F00E70D5B /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4BDB87E41DC0423F00E70D5B; - remoteInfo = NextGrowingTextView; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 4B26B0FB27CF767A00DDBEC0 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 4B26B0F827CF767A00DDBEC0 /* NextGrowingTextView.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ - 4B26B0D027CF60B900DDBEC0 /* Book.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Book.swift; sourceTree = ""; }; - 4B26B0D227CF60E200DDBEC0 /* RootContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootContainerViewController.swift; sourceTree = ""; }; - 4B26B0D827CF761700DDBEC0 /* NextGrowingTextView-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NextGrowingTextView-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4B26B0FC27CF775200DDBEC0 /* FlexibleWidthCenteringViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleWidthCenteringViewController.swift; sourceTree = ""; }; - 4B26B10D27CF7A8B00DDBEC0 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - 4B26B34927CF848100DDBEC0 /* SizingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizingContainerView.swift; sourceTree = ""; }; - 4B26B34B27CF87A400DDBEC0 /* NextGrowingTextView.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = NextGrowingTextView.podspec; sourceTree = ""; }; - 4B26B34D27CFD12D00DDBEC0 /* UIControl+Closure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Closure.swift"; sourceTree = ""; }; - 4B26B34F27CFD31500DDBEC0 /* FixedWidthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedWidthViewController.swift; sourceTree = ""; }; - 4BDB87E51DC0423F00E70D5B /* NextGrowingTextView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NextGrowingTextView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4BDB87F01DC0425600E70D5B /* InternalTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalTextView.swift; sourceTree = ""; }; - 4BDB87F11DC0425600E70D5B /* NextGrowingTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = NextGrowingTextView.swift; sourceTree = ""; tabWidth = 2; }; - 4BDB87FA1DC0426A00E70D5B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 4BDB88011DC0426A00E70D5B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 4BDB880F1DC042F700E70D5B /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + B01DEB2B2FFE1289009452C9 /* GrowingTextViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GrowingTextViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B01DEB2C2FFE1289009452C9 /* GrowingTextViewDemo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GrowingTextViewDemo; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ - 4B26B0D527CF761700DDBEC0 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 4B26B10227CF77B600DDBEC0 /* MondrianLayout in Frameworks */, - 4B26B10727CF782000DDBEC0 /* CompositionKit in Frameworks */, - 4B26B0F427CF767A00DDBEC0 /* StorybookKit in Frameworks */, - 4B26B10C27CF7A6B00DDBEC0 /* TypedTextAttributes in Frameworks */, - 4B26B11027CF7E9A00DDBEC0 /* Wrap in Frameworks */, - 4B26B0F727CF767A00DDBEC0 /* NextGrowingTextView.framework in Frameworks */, - 4B26B0F627CF767A00DDBEC0 /* StorybookUI in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4BDB87E11DC0423F00E70D5B /* Frameworks */ = { + B01DEB282FFE1289009452C9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B01DEB372FFE1352009452C9 /* StorybookKit in Frameworks */, + B01DEB392FFE135A009452C9 /* NextGrowingTextView in Frameworks */, + B01DEB3B2FFE135A009452C9 /* NextGrowingTextViewSwiftUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4B26B0D927CF761700DDBEC0 /* NextGrowingTextView-Demo */ = { - isa = PBXGroup; - children = ( - 4B26B34D27CFD12D00DDBEC0 /* UIControl+Closure.swift */, - 4BDB87FA1DC0426A00E70D5B /* AppDelegate.swift */, - 4BDB88011DC0426A00E70D5B /* Assets.xcassets */, - 4BDB880F1DC042F700E70D5B /* Launch Screen.storyboard */, - 4B26B0D027CF60B900DDBEC0 /* Book.swift */, - 4B26B0D227CF60E200DDBEC0 /* RootContainerViewController.swift */, - 4B26B0FC27CF775200DDBEC0 /* FlexibleWidthCenteringViewController.swift */, - 4B26B34F27CFD31500DDBEC0 /* FixedWidthViewController.swift */, - ); - path = "NextGrowingTextView-Demo"; - sourceTree = ""; - }; 4B26B0F227CF767A00DDBEC0 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -120,10 +44,7 @@ 4BDB87DB1DC0423F00E70D5B = { isa = PBXGroup; children = ( - 4B26B10D27CF7A8B00DDBEC0 /* Package.swift */, - 4B26B34B27CF87A400DDBEC0 /* NextGrowingTextView.podspec */, - 4BDB87E71DC0423F00E70D5B /* NextGrowingTextView */, - 4B26B0D927CF761700DDBEC0 /* NextGrowingTextView-Demo */, + B01DEB2C2FFE1289009452C9 /* GrowingTextViewDemo */, 4BDB87E61DC0423F00E70D5B /* Products */, 4B26B0F227CF767A00DDBEC0 /* Frameworks */, ); @@ -134,81 +55,38 @@ 4BDB87E61DC0423F00E70D5B /* Products */ = { isa = PBXGroup; children = ( - 4BDB87E51DC0423F00E70D5B /* NextGrowingTextView.framework */, - 4B26B0D827CF761700DDBEC0 /* NextGrowingTextView-Demo.app */, + B01DEB2B2FFE1289009452C9 /* GrowingTextViewDemo.app */, ); name = Products; sourceTree = ""; }; - 4BDB87E71DC0423F00E70D5B /* NextGrowingTextView */ = { - isa = PBXGroup; - children = ( - 4BDB87F01DC0425600E70D5B /* InternalTextView.swift */, - 4BDB87F11DC0425600E70D5B /* NextGrowingTextView.swift */, - 4B26B34927CF848100DDBEC0 /* SizingContainerView.swift */, - ); - path = NextGrowingTextView; - sourceTree = ""; - }; /* End PBXGroup section */ -/* Begin PBXHeadersBuildPhase section */ - 4BDB87E21DC0423F00E70D5B /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - /* Begin PBXNativeTarget section */ - 4B26B0D727CF761700DDBEC0 /* NextGrowingTextView-Demo */ = { + B01DEB2A2FFE1289009452C9 /* GrowingTextViewDemo */ = { isa = PBXNativeTarget; - buildConfigurationList = 4B26B0E927CF761800DDBEC0 /* Build configuration list for PBXNativeTarget "NextGrowingTextView-Demo" */; + buildConfigurationList = B01DEB332FFE128A009452C9 /* Build configuration list for PBXNativeTarget "GrowingTextViewDemo" */; buildPhases = ( - 4B26B0D427CF761700DDBEC0 /* Sources */, - 4B26B0D527CF761700DDBEC0 /* Frameworks */, - 4B26B0D627CF761700DDBEC0 /* Resources */, - 4B26B0FB27CF767A00DDBEC0 /* Embed Frameworks */, + B01DEB272FFE1289009452C9 /* Sources */, + B01DEB282FFE1289009452C9 /* Frameworks */, + B01DEB292FFE1289009452C9 /* Resources */, ); buildRules = ( ); dependencies = ( - 4B26B0FA27CF767A00DDBEC0 /* PBXTargetDependency */, - ); - name = "NextGrowingTextView-Demo"; - packageProductDependencies = ( - 4B26B0F327CF767A00DDBEC0 /* StorybookKit */, - 4B26B0F527CF767A00DDBEC0 /* StorybookUI */, - 4B26B10127CF77B600DDBEC0 /* MondrianLayout */, - 4B26B10627CF782000DDBEC0 /* CompositionKit */, - 4B26B10B27CF7A6B00DDBEC0 /* TypedTextAttributes */, - 4B26B10F27CF7E9A00DDBEC0 /* Wrap */, - ); - productName = "NextGrowingTextView-Demo"; - productReference = 4B26B0D827CF761700DDBEC0 /* NextGrowingTextView-Demo.app */; - productType = "com.apple.product-type.application"; - }; - 4BDB87E41DC0423F00E70D5B /* NextGrowingTextView */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4BDB87ED1DC0423F00E70D5B /* Build configuration list for PBXNativeTarget "NextGrowingTextView" */; - buildPhases = ( - 4BDB87E01DC0423F00E70D5B /* Sources */, - 4BDB87E11DC0423F00E70D5B /* Frameworks */, - 4BDB87E21DC0423F00E70D5B /* Headers */, - 4BDB87E31DC0423F00E70D5B /* Resources */, ); - buildRules = ( + fileSystemSynchronizedGroups = ( + B01DEB2C2FFE1289009452C9 /* GrowingTextViewDemo */, ); - dependencies = ( - ); - name = NextGrowingTextView; + name = GrowingTextViewDemo; packageProductDependencies = ( + B01DEB362FFE1352009452C9 /* StorybookKit */, + B01DEB382FFE135A009452C9 /* NextGrowingTextView */, + B01DEB3A2FFE135A009452C9 /* NextGrowingTextViewSwiftUI */, ); - productName = NextGrowingTextView; - productReference = 4BDB87E51DC0423F00E70D5B /* NextGrowingTextView.framework */; - productType = "com.apple.product-type.framework"; + productName = GrowingTextViewDemo; + productReference = B01DEB2B2FFE1289009452C9 /* GrowingTextViewDemo.app */; + productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -216,18 +94,12 @@ 4BDB87DC1DC0423F00E70D5B /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 2660; LastUpgradeCheck = 1300; ORGANIZATIONNAME = muukii; TargetAttributes = { - 4B26B0D727CF761700DDBEC0 = { - CreatedOnToolsVersion = 13.3; - }; - 4BDB87E41DC0423F00E70D5B = { - CreatedOnToolsVersion = 8.0; - DevelopmentTeam = KU2QEJ9K3Z; - LastSwiftMigration = 0800; - ProvisioningStyle = Automatic; + B01DEB2A2FFE1289009452C9 = { + CreatedOnToolsVersion = 26.6; }; }; }; @@ -242,33 +114,20 @@ ); mainGroup = 4BDB87DB1DC0423F00E70D5B; packageReferences = ( - 4B26B0CB27CF606400DDBEC0 /* XCRemoteSwiftPackageReference "Storybook-ios" */, - 4B26B0FE27CF77A900DDBEC0 /* XCRemoteSwiftPackageReference "MondrianLayout" */, - 4B26B10327CF780F00DDBEC0 /* XCRemoteSwiftPackageReference "CompositionKit" */, - 4B26B10827CF7A5D00DDBEC0 /* XCRemoteSwiftPackageReference "TypedTextAttributes" */, - 4B26B10E27CF7E9A00DDBEC0 /* XCRemoteSwiftPackageReference "Wrap" */, + B01DEB212FFE1206009452C9 /* XCRemoteSwiftPackageReference "swift-storybook" */, + B01DEB222FFE126F009452C9 /* XCLocalSwiftPackageReference "../NextGrowingTextView" */, ); productRefGroup = 4BDB87E61DC0423F00E70D5B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 4BDB87E41DC0423F00E70D5B /* NextGrowingTextView */, - 4B26B0D727CF761700DDBEC0 /* NextGrowingTextView-Demo */, + B01DEB2A2FFE1289009452C9 /* GrowingTextViewDemo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 4B26B0D627CF761700DDBEC0 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 4B26B0EF27CF766A00DDBEC0 /* Launch Screen.storyboard in Resources */, - 4B26B0EE27CF766A00DDBEC0 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4BDB87E31DC0423F00E70D5B /* Resources */ = { + B01DEB292FFE1289009452C9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -278,105 +137,16 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 4B26B0D427CF761700DDBEC0 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 4B26B0EC27CF766A00DDBEC0 /* AppDelegate.swift in Sources */, - 4B26B0FD27CF775200DDBEC0 /* FlexibleWidthCenteringViewController.swift in Sources */, - 4B26B0F127CF766A00DDBEC0 /* RootContainerViewController.swift in Sources */, - 4B26B0F027CF766A00DDBEC0 /* Book.swift in Sources */, - 4B26B35027CFD31500DDBEC0 /* FixedWidthViewController.swift in Sources */, - 4B26B34E27CFD12D00DDBEC0 /* UIControl+Closure.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4BDB87E01DC0423F00E70D5B /* Sources */ = { + B01DEB272FFE1289009452C9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4BDB87F31DC0425600E70D5B /* NextGrowingTextView.swift in Sources */, - 4B26B34A27CF848100DDBEC0 /* SizingContainerView.swift in Sources */, - 4BDB87F21DC0425600E70D5B /* InternalTextView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 4B26B0FA27CF767A00DDBEC0 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4BDB87E41DC0423F00E70D5B /* NextGrowingTextView */; - targetProxy = 4B26B0F927CF767A00DDBEC0 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ - 4B26B0EA27CF761800DDBEC0 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "app.muukii.NextGrowingTextView-Demo"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 4B26B0EB27CF761800DDBEC0 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "app.muukii.NextGrowingTextView-Demo"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; 4BDB87EB1DC0423F00E70D5B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -501,68 +271,95 @@ }; name = Release; }; - 4BDB87EE1DC0423F00E70D5B /* Debug */ = { + B01DEB342FFE128A009452C9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - DEFINES_MODULE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KU2QEJ9K3Z; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", - "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = me.muuki.NextGrowingTextView; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.muukii.GrowingTextViewDemo; PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + 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; }; - 4BDB87EF1DC0423F00E70D5B /* Release */ = { + B01DEB352FFE128A009452C9 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_CODE_COVERAGE = NO; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - DEFINES_MODULE = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KU2QEJ9K3Z; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", - "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = me.muuki.NextGrowingTextView; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.muukii.GrowingTextViewDemo; PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; + 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; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 4B26B0E927CF761800DDBEC0 /* Build configuration list for PBXNativeTarget "NextGrowingTextView-Demo" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4B26B0EA27CF761800DDBEC0 /* Debug */, - 4B26B0EB27CF761800DDBEC0 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 4BDB87DF1DC0423F00E70D5B /* Build configuration list for PBXProject "NextGrowingTextView" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -572,90 +369,50 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4BDB87ED1DC0423F00E70D5B /* Build configuration list for PBXNativeTarget "NextGrowingTextView" */ = { + B01DEB332FFE128A009452C9 /* Build configuration list for PBXNativeTarget "GrowingTextViewDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( - 4BDB87EE1DC0423F00E70D5B /* Debug */, - 4BDB87EF1DC0423F00E70D5B /* Release */, + B01DEB342FFE128A009452C9 /* Debug */, + B01DEB352FFE128A009452C9 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 4B26B0CB27CF606400DDBEC0 /* XCRemoteSwiftPackageReference "Storybook-ios" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/eure/Storybook-ios"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; +/* Begin XCLocalSwiftPackageReference section */ + B01DEB222FFE126F009452C9 /* XCLocalSwiftPackageReference "../NextGrowingTextView" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../NextGrowingTextView; }; - 4B26B0FE27CF77A900DDBEC0 /* XCRemoteSwiftPackageReference "MondrianLayout" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/muukii/MondrianLayout.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.0; - }; - }; - 4B26B10327CF780F00DDBEC0 /* XCRemoteSwiftPackageReference "CompositionKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/muukii/CompositionKit.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.2.1; - }; - }; - 4B26B10827CF7A5D00DDBEC0 /* XCRemoteSwiftPackageReference "TypedTextAttributes" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/muukii/TypedTextAttributes.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - 4B26B10E27CF7E9A00DDBEC0 /* XCRemoteSwiftPackageReference "Wrap" */ = { +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + B01DEB212FFE1206009452C9 /* XCRemoteSwiftPackageReference "swift-storybook" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/VergeGroup/Wrap.git"; + repositoryURL = "https://github.com/eure/swift-storybook"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 3.0.0; + minimumVersion = 2.9.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 4B26B0F327CF767A00DDBEC0 /* StorybookKit */ = { + B01DEB362FFE1352009452C9 /* StorybookKit */ = { isa = XCSwiftPackageProductDependency; - package = 4B26B0CB27CF606400DDBEC0 /* XCRemoteSwiftPackageReference "Storybook-ios" */; + package = B01DEB212FFE1206009452C9 /* XCRemoteSwiftPackageReference "swift-storybook" */; productName = StorybookKit; }; - 4B26B0F527CF767A00DDBEC0 /* StorybookUI */ = { - isa = XCSwiftPackageProductDependency; - package = 4B26B0CB27CF606400DDBEC0 /* XCRemoteSwiftPackageReference "Storybook-ios" */; - productName = StorybookUI; - }; - 4B26B10127CF77B600DDBEC0 /* MondrianLayout */ = { + B01DEB382FFE135A009452C9 /* NextGrowingTextView */ = { isa = XCSwiftPackageProductDependency; - package = 4B26B0FE27CF77A900DDBEC0 /* XCRemoteSwiftPackageReference "MondrianLayout" */; - productName = MondrianLayout; - }; - 4B26B10627CF782000DDBEC0 /* CompositionKit */ = { - isa = XCSwiftPackageProductDependency; - package = 4B26B10327CF780F00DDBEC0 /* XCRemoteSwiftPackageReference "CompositionKit" */; - productName = CompositionKit; - }; - 4B26B10B27CF7A6B00DDBEC0 /* TypedTextAttributes */ = { - isa = XCSwiftPackageProductDependency; - package = 4B26B10827CF7A5D00DDBEC0 /* XCRemoteSwiftPackageReference "TypedTextAttributes" */; - productName = TypedTextAttributes; + package = B01DEB222FFE126F009452C9 /* XCLocalSwiftPackageReference "../NextGrowingTextView" */; + productName = NextGrowingTextView; }; - 4B26B10F27CF7E9A00DDBEC0 /* Wrap */ = { + B01DEB3A2FFE135A009452C9 /* NextGrowingTextViewSwiftUI */ = { isa = XCSwiftPackageProductDependency; - package = 4B26B10E27CF7E9A00DDBEC0 /* XCRemoteSwiftPackageReference "Wrap" */; - productName = Wrap; + package = B01DEB222FFE126F009452C9 /* XCLocalSwiftPackageReference "../NextGrowingTextView" */; + productName = NextGrowingTextViewSwiftUI; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/NextGrowingTextView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NextGrowingTextView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 509144a..016b38c 100644 --- a/NextGrowingTextView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NextGrowingTextView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,79 +1,78 @@ { - "object": { - "pins": [ - { - "package": "CompositionKit", - "repositoryURL": "https://github.com/muukii/CompositionKit.git", - "state": { - "branch": null, - "revision": "a3153e6afe3260d71f7b95d48d5cbe913dc450b9", - "version": "0.2.1" - } - }, - { - "package": "Descriptors", - "repositoryURL": "https://github.com/muukii/Descriptors.git", - "state": { - "branch": null, - "revision": "b93f250d6e4007d512da77b1138acd03c11d4550", - "version": "0.1.0" - } - }, - { - "package": "MondrianLayout", - "repositoryURL": "https://github.com/muukii/MondrianLayout.git", - "state": { - "branch": null, - "revision": "239bc0b02b8abbc211720476d1554b736832bb9c", - "version": "0.9.0" - } - }, - { - "package": "RxSwift", - "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", - "state": { - "branch": null, - "revision": "b4307ba0b6425c0ba4178e138799946c3da594f8", - "version": "6.5.0" - } - }, - { - "package": "Storybook", - "repositoryURL": "https://github.com/eure/Storybook-ios", - "state": { - "branch": null, - "revision": "48cd0bbecd40edabcce32e819c5a23014a49013b", - "version": "1.9.0" - } - }, - { - "package": "TypedTextAttributes", - "repositoryURL": "https://github.com/muukii/TypedTextAttributes.git", - "state": { - "branch": null, - "revision": "eabf9c7985e13d17a70db53d27d584f39bd10830", - "version": "1.3.1" - } - }, - { - "package": "Verge", - "repositoryURL": "https://github.com/VergeGroup/Verge", - "state": { - "branch": null, - "revision": "6c650bc080a2d2ebf0be66fe5dbcded013d4b3ca", - "version": "8.15.2" - } - }, - { - "package": "Wrap", - "repositoryURL": "https://github.com/VergeGroup/Wrap.git", - "state": { - "branch": null, - "revision": "2d2cc9091881b2758e33f97c073039a6237b3275", - "version": "3.0.0" - } + "originHash" : "42d8c7a3bd510b04da36095981399a7da8d5e1cc56509b7ac2ee796a16cfdf31", + "pins" : [ + { + "identity" : "descriptors", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/Descriptors", + "state" : { + "revision" : "1ec4fcd01b7e3a6df9dfafcca8155e297a7a1d9f", + "version" : "0.3.0" } - ] - }, - "version": 1 + }, + { + "identity" : "resultbuilderkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/ResultBuilderKit", + "state" : { + "revision" : "34aa57fcee5ec3ee0f368b05cb53c7919c188ab2", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-storybook", + "kind" : "remoteSourceControl", + "location" : "https://github.com/eure/swift-storybook", + "state" : { + "revision" : "29ebe3924ae1cd54a0de93e16db79fecc0181e7d", + "version" : "2.9.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" + } + }, + { + "identity" : "swiftui-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/swiftui-support", + "state" : { + "revision" : "f8058066c1d310458390d6c73462bf6722b71fc9", + "version" : "0.14.0" + } + }, + { + "identity" : "texture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/Texture.git", + "state" : { + "revision" : "9620825e5f97dfbd1ce653443e5165d5eb405b9b", + "version" : "3.0.4" + } + }, + { + "identity" : "texturebridging", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/TextureBridging.git", + "state" : { + "revision" : "4383f8a9846a0507d2c22ee9fac22153f9b86fed", + "version" : "3.2.1" + } + }, + { + "identity" : "textureswiftsupport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/FluidGroup/TextureSwiftSupport.git", + "state" : { + "revision" : "ce3319351831533330e22d205bd83937848059db", + "version" : "3.24.0" + } + } + ], + "version" : 3 } diff --git a/NextGrowingTextViewSwiftUI/GrowingTextEditor.swift b/NextGrowingTextViewSwiftUI/GrowingTextEditor.swift new file mode 100644 index 0000000..960d3e3 --- /dev/null +++ b/NextGrowingTextViewSwiftUI/GrowingTextEditor.swift @@ -0,0 +1,292 @@ +import SwiftUI + +/// Layout rules for a SwiftUI text editor that grows with its text content. +/// +/// The editor stays at `minLines` while short or empty, expands as text wraps, +/// and caps at `maxLines` when provided. Once capped, SwiftUI's `TextEditor` +/// owns the inner scrolling behavior. +@available(iOS 16.0, *) +public struct GrowingTextEditorConfiguration: Sendable, Equatable { + + /// The smallest number of visual lines the editor should reserve. + public var minLines: Int + + /// The largest number of visual lines before the editor starts scrolling. + /// + /// Use `nil` for an uncapped editor that keeps expanding with its content. + public var maxLines: Int? + + /// Horizontal space applied around both the visible editor and the hidden + /// measuring editor. + public var horizontalPadding: CGFloat + + /// Vertical space applied around both the visible editor and the hidden + /// measuring editor. + public var verticalPadding: CGFloat + + /// Extra spacing inserted between rendered text lines. + public var lineSpacing: CGFloat + + /// Creates layout rules for a growing SwiftUI text editor. + public init( + minLines: Int = 1, + maxLines: Int? = nil, + horizontalPadding: CGFloat = 5, + verticalPadding: CGFloat = 8, + lineSpacing: CGFloat = 0 + ) { + let resolvedMinLines = Swift.max(1, minLines) + self.minLines = resolvedMinLines + self.maxLines = maxLines.map { Swift.max(resolvedMinLines, $0) } + self.horizontalPadding = horizontalPadding + self.verticalPadding = verticalPadding + self.lineSpacing = lineSpacing + } + + var contentInsets: EdgeInsets { + EdgeInsets( + top: verticalPadding, + leading: horizontalPadding, + bottom: verticalPadding, + trailing: horizontalPadding + ) + } +} + +/// A pure SwiftUI growing text input built on top of `TextEditor`. +/// +/// The implementation measures a hidden, non-scrolling `TextEditor` with the +/// same font, line spacing, and padding as the visible editor, then applies the +/// measured height back to the visible `TextEditor`. It intentionally does not +/// bridge to `UITextView`, which means UIKit-only hooks such as content offset +/// control and flashing scroll indicators are outside this component's contract. +@available(iOS 16.0, *) +public struct GrowingTextEditor: View { + + @Binding private var text: String + + @Environment(\.font) private var environmentFont + @State private var measurements = GrowingTextEditorMeasurements() + + private let configuration: GrowingTextEditorConfiguration + private let explicitFont: Font? + private let placeholder: Placeholder + + /// Creates a growing text editor with custom placeholder content. + public init( + text: Binding, + configuration: GrowingTextEditorConfiguration = GrowingTextEditorConfiguration(), + font: Font? = nil, + @ViewBuilder placeholder: () -> Placeholder + ) { + self._text = text + self.configuration = configuration + self.explicitFont = font + self.placeholder = placeholder() + } + + public var body: some View { + ZStack(alignment: .topLeading) { + if text.isEmpty { + placeholder + .foregroundStyle(.secondary) + .padding(placeholderInsets) + .allowsHitTesting(false) + .accessibilityHidden(true) + } + + editor + .frame(height: resolvedHeight) + } + .background(alignment: .topLeading) { + measurementLayer + } + .onPreferenceChange(GrowingTextEditorHeightPreferenceKey.self) { values in + measurements.update(with: values) + } + } + + @ViewBuilder + private var editor: some View { + TextEditor(text: $text) + .font(resolvedFont) + .lineSpacing(configuration.lineSpacing) + .scrollContentBackground(.hidden) + .padding(configuration.contentInsets) + } + + private var measurementLayer: some View { + ZStack(alignment: .topLeading) { + measurementEditor( + GrowingTextEditorMeasurementText.content(for: text), + role: .content + ) + measurementEditor( + GrowingTextEditorMeasurementText.lines(configuration.minLines), + role: .minimum + ) + if let maxLines = configuration.maxLines { + measurementEditor( + GrowingTextEditorMeasurementText.lines(maxLines), + role: .maximum + ) + } + } + .allowsHitTesting(false) + .accessibilityHidden(true) + } + + private func measurementEditor( + _ value: String, + role: GrowingTextEditorMeasurementRole + ) -> some View { + TextEditor(text: .constant(value)) + .font(resolvedFont) + .lineSpacing(configuration.lineSpacing) + .scrollContentBackground(.hidden) + .scrollDisabled(true) + .padding(configuration.contentInsets) + .frame(maxWidth: .infinity, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .hidden() + .readGrowingTextEditorHeight(for: role) + } + + private var resolvedFont: Font { + explicitFont ?? environmentFont ?? .body + } + + private var placeholderInsets: EdgeInsets { + EdgeInsets( + top: configuration.verticalPadding + GrowingTextEditorTextEditorInset.top, + leading: configuration.horizontalPadding + GrowingTextEditorTextEditorInset.leading, + bottom: configuration.verticalPadding + GrowingTextEditorTextEditorInset.bottom, + trailing: configuration.horizontalPadding + GrowingTextEditorTextEditorInset.trailing + ) + } + + private var resolvedHeight: CGFloat { + let minimum = measurements.minimum ?? measurements.content ?? 1 + let content = Swift.max(measurements.content ?? minimum, minimum) + + if let maximum = measurements.maximum { + return Swift.min(content, Swift.max(maximum, minimum)) + } else { + return content + } + } +} + +@available(iOS 16.0, *) +public extension GrowingTextEditor where Placeholder == EmptyView { + + /// Creates an editor without placeholder content. + init( + text: Binding, + configuration: GrowingTextEditorConfiguration = GrowingTextEditorConfiguration(), + font: Font? = nil + ) { + self._text = text + self.configuration = configuration + self.explicitFont = font + self.placeholder = EmptyView() + } +} + +@available(iOS 16.0, *) +public extension GrowingTextEditor where Placeholder == Text { + + /// Creates an editor with a localizable text placeholder. + init( + text: Binding, + placeholder: LocalizedStringKey, + configuration: GrowingTextEditorConfiguration = GrowingTextEditorConfiguration(), + font: Font? = nil + ) { + self._text = text + self.configuration = configuration + self.explicitFont = font + self.placeholder = Text(placeholder) + } +} + +/// A `TextView`-named alias for the SwiftUI growing editor. +@available(iOS 16.0, *) +public typealias GrowingTextView = GrowingTextEditor + +@available(iOS 16.0, *) +private struct GrowingTextEditorMeasurements: Equatable { + var content: CGFloat? + var minimum: CGFloat? + var maximum: CGFloat? + + mutating func update(with values: [GrowingTextEditorMeasurementRole: CGFloat]) { + content = values[.content] ?? content + minimum = values[.minimum] ?? minimum + maximum = values[.maximum] ?? maximum + } +} + +@available(iOS 16.0, *) +private enum GrowingTextEditorMeasurementRole: Hashable { + case content + case minimum + case maximum +} + +@available(iOS 16.0, *) +private enum GrowingTextEditorTextEditorInset { + /// Approximate default inset SwiftUI's `TextEditor` keeps between its outer + /// bounds and first rendered glyph. SwiftUI does not expose this value, so the + /// placeholder uses the same small adjustment the previous Journal editor + /// applied by hand. + static let top: CGFloat = 8 + static let leading: CGFloat = 4 + static let bottom: CGFloat = 8 + static let trailing: CGFloat = 4 +} + +@available(iOS 16.0, *) +private enum GrowingTextEditorMeasurementText { + + static func content(for text: String) -> String { + let value = text.isEmpty ? " " : text + + if value.hasSuffix("\n") { + return value + " " + } else { + return value + } + } + + static func lines(_ count: Int) -> String { + Array(repeating: " ", count: Swift.max(1, count)).joined(separator: "\n") + } +} + +@available(iOS 16.0, *) +private struct GrowingTextEditorHeightPreferenceKey: PreferenceKey { + static let defaultValue: [GrowingTextEditorMeasurementRole: CGFloat] = [:] + + static func reduce( + value: inout [GrowingTextEditorMeasurementRole: CGFloat], + nextValue: () -> [GrowingTextEditorMeasurementRole: CGFloat] + ) { + value.merge(nextValue(), uniquingKeysWith: { _, new in new }) + } +} + +@available(iOS 16.0, *) +private extension View { + + func readGrowingTextEditorHeight(for role: GrowingTextEditorMeasurementRole) -> some View { + background { + GeometryReader { proxy in + Color.clear.preference( + key: GrowingTextEditorHeightPreferenceKey.self, + value: [role: proxy.size.height] + ) + } + } + } +} diff --git a/Package.swift b/Package.swift index 9ae9006..c9e4ccc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,25 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "NextGrowingTextView", platforms: [ - .iOS(.v10) + .iOS(.v16) ], products: [ - .library(name: "NextGrowingTextView", targets: ["NextGrowingTextView"]) + .library(name: "NextGrowingTextView", targets: ["NextGrowingTextView"]), + .library(name: "NextGrowingTextViewSwiftUI", targets: ["NextGrowingTextViewSwiftUI"]), ], targets: [ .target( name: "NextGrowingTextView", path: "NextGrowingTextView" + ), + .target( + name: "NextGrowingTextViewSwiftUI", + path: "NextGrowingTextViewSwiftUI" ) - ] + ], + swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index 5ffe2bd..d856310 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,44 @@ growingTextView.textView growingTextView.placeholderLabel ``` +### SwiftUI + +The Swift Package product `NextGrowingTextViewSwiftUI` provides a pure SwiftUI +implementation for iOS 16 and later. It measures a hidden `TextEditor` with the +same font, line spacing, and padding as the visible editor, then applies the +measured height to the visible editor. + +```swift +import NextGrowingTextViewSwiftUI + +@State private var text = "" + +GrowingTextEditor( + text: $text, + placeholder: "Write something...", + configuration: GrowingTextEditorConfiguration( + minLines: 1, + maxLines: 6, + horizontalPadding: 8, + verticalPadding: 4, + lineSpacing: 3 + ), + font: .body +) +``` + +`GrowingTextView` is also available as a typealias when call sites prefer the +TextView naming. + ## Requirements -iOS 9.0+ Swift 5.5+ +- UIKit API: iOS 9.0+ Swift 5.5+ +- Swift Package Manager products: iOS 16.0+ +- SwiftUI `GrowingTextEditor` API: iOS 16.0+ ## Installation -- Supports the following: - - CocoaPods - - Swift Package Manager +Swift Package Manager is the supported installation method. ## Author From abe949416f7f40781f7371cd1b15091a21a53bff Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 8 Jul 2026 16:21:37 +0900 Subject: [PATCH 2/5] Add SwiftUI preview image to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d856310..dd4c07c 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ implementation for iOS 16 and later. It measures a hidden `TextEditor` with the same font, line spacing, and padding as the visible editor, then applies the measured height to the visible editor. +

+ Messages-style SwiftUI GrowingTextEditor preview +

+ ```swift import NextGrowingTextViewSwiftUI From c92db3ec43c2ddb1c1b47d011f8179bfd25fd3a2 Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 8 Jul 2026 16:28:09 +0900 Subject: [PATCH 3/5] Update GitHub Actions CI --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f027c..28a93ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,27 @@ name: CI -on: [push, pull_request, workflow_dispatch] + +on: + push: + pull_request: + workflow_dispatch: + jobs: - test: - runs-on: macos-11 + build: + name: Build GrowingTextViewDemo + runs-on: macos-26 + steps: - - uses: maxim-lobanov/setup-xcode@v1.1 + - uses: actions/checkout@v7 + + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1.7.0 with: - xcode-version: "13.2" - - uses: actions/checkout@v2 - # - run: set -o pipefail && xcodebuild build -project NextGrowingTextView.xcodeproj -scheme NextGrowingTextView -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty - - run: pod lib lint --allow-warnings + xcode-version: "26.6" + + - name: Build + run: | + xcodebuild build \ + -project NextGrowingTextView.xcodeproj \ + -scheme GrowingTextViewDemo \ + -destination "generic/platform=iOS Simulator" \ + CODE_SIGNING_ALLOWED=NO From be8630d559ebeedb00b110740bb9c82ca1eecf59 Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 8 Jul 2026 16:31:03 +0900 Subject: [PATCH 4/5] Allow macro plugins in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28a93ff..8d32ef2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,5 @@ jobs: -project NextGrowingTextView.xcodeproj \ -scheme GrowingTextViewDemo \ -destination "generic/platform=iOS Simulator" \ + -skipMacroValidation \ CODE_SIGNING_ALLOWED=NO From ba72ae77d9bf9bf0062d7ba7df45d8a0fee7502e Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 8 Jul 2026 16:34:02 +0900 Subject: [PATCH 5/5] Update --- .../SwiftUIGrowingTextEditorPreviews.swift | 189 +----------------- 1 file changed, 6 insertions(+), 183 deletions(-) diff --git a/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift b/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift index 293d451..a0645f4 100644 --- a/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift +++ b/GrowingTextViewDemo/SwiftUIGrowingTextEditorPreviews.swift @@ -92,194 +92,14 @@ struct SwiftUIGrowingTextEditorMessagesPreview: View { This composer uses GrowingTextEditor, so the input expands as the message gets longer. """ - var body: some View { - ZStack { - SwiftUIGrowingTextEditorMessagesBackdrop() - - VStack(spacing: 0) { - SwiftUIGrowingTextEditorMessagesHeader( - contactName: "Aoi", - status: "iMessage" - ) - - SwiftUIGrowingTextEditorMessagesTranscript( - messages: SwiftUIGrowingTextEditorMessage.samples - ) - - SwiftUIGrowingTextEditorMessagesComposer(text: $draft) - } - .background(.regularMaterial) - } - .background(Color(.systemGroupedBackground)) - } -} - -/// Atmospheric background used behind the Messages-style phone surface. -private struct SwiftUIGrowingTextEditorMessagesBackdrop: View { - - var body: some View { - LinearGradient( - colors: [ - Color(red: 0.78, green: 0.90, blue: 1.00), - Color(red: 0.95, green: 0.97, blue: 1.00), - Color(red: 0.95, green: 0.92, blue: 0.86), - ], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .overlay(alignment: .topTrailing) { - Circle() - .fill(.blue.opacity(0.16)) - .frame(width: 220, height: 220) - .blur(radius: 18) - .offset(x: 80, y: -80) - } - .overlay(alignment: .bottomLeading) { - Circle() - .fill(.mint.opacity(0.18)) - .frame(width: 180, height: 180) - .blur(radius: 20) - .offset(x: -70, y: 70) - } - } -} - -/// Header chrome for the Messages-style preview. -private struct SwiftUIGrowingTextEditorMessagesHeader: View { - - let contactName: String - let status: String - - var body: some View { - HStack(spacing: 12) { - Image(systemName: "chevron.left") - .font(.headline.weight(.semibold)) - .foregroundStyle(.blue) - - Spacer() - - VStack(spacing: 5) { - ZStack { - Circle() - .fill( - LinearGradient( - colors: [.cyan, .blue], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - - Text(String(contactName.prefix(1))) - .font(.headline.weight(.semibold)) - .foregroundStyle(.white) - } - .frame(width: 42, height: 42) - - VStack(spacing: 1) { - Text(contactName) - .font(.caption.weight(.semibold)) - - Text(status) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: "video") - .font(.headline.weight(.semibold)) - .foregroundStyle(.blue) - } - .padding(.horizontal, 16) - .padding(.top, 14) - .padding(.bottom, 10) - .background(.thinMaterial) - } -} - -/// Scrollable transcript for the Messages-style preview. -private struct SwiftUIGrowingTextEditorMessagesTranscript: View { - - let messages: [SwiftUIGrowingTextEditorMessage] - var body: some View { ScrollView { - VStack(spacing: 10) { - Text("Today 14:32") - .font(.caption2.weight(.medium)) - .foregroundStyle(.secondary) - .padding(.vertical, 8) - - ForEach(messages) { message in - SwiftUIGrowingTextEditorMessageBubble(message: message) - } - } - .padding(.horizontal, 14) - .padding(.bottom, 18) } - .scrollIndicators(.hidden) - } -} - -/// One transcript bubble in the Messages-style preview. -private struct SwiftUIGrowingTextEditorMessageBubble: View { - - let message: SwiftUIGrowingTextEditorMessage - - var body: some View { - HStack(alignment: .bottom, spacing: 6) { - if message.isOutgoing { - Spacer(minLength: 42) - } + .safeAreaInset(edge: .bottom) { + SwiftUIGrowingTextEditorMessagesComposer(text: $draft) - Text(message.text) - .font(.body) - .foregroundStyle(message.isOutgoing ? .white : .primary) - .padding(.horizontal, 14) - .padding(.vertical, 10) - .background(messageBackground, in: bubbleShape) - .shadow(color: .black.opacity(message.isOutgoing ? 0.14 : 0.06), radius: 4, x: 0, y: 2) - - if message.isOutgoing == false { - Spacer(minLength: 42) - } } - .frame(maxWidth: .infinity, alignment: message.isOutgoing ? .trailing : .leading) - } - private var messageBackground: some ShapeStyle { - if message.isOutgoing { - return AnyShapeStyle( - LinearGradient( - colors: [.blue, Color(red: 0.10, green: 0.45, blue: 1.00)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - } else { - return AnyShapeStyle(.white.opacity(0.92)) - } - } - - private var bubbleShape: UnevenRoundedRectangle { - if message.isOutgoing { - UnevenRoundedRectangle( - topLeadingRadius: 20, - bottomLeadingRadius: 20, - bottomTrailingRadius: 6, - topTrailingRadius: 20, - style: .continuous - ) - } else { - UnevenRoundedRectangle( - topLeadingRadius: 20, - bottomLeadingRadius: 6, - bottomTrailingRadius: 20, - topTrailingRadius: 20, - style: .continuous - ) - } } } @@ -332,7 +152,10 @@ private struct SwiftUIGrowingTextEditorMessagesComposer: View { .padding(.horizontal, 12) .padding(.top, 10) .padding(.bottom, 14) - .background(.ultraThinMaterial) + .background( + RoundedRectangle(cornerRadius: 32).foregroundStyle(.thinMaterial) + ) + .padding(16) } }