User report: 'every first time to tap to input box nothing
happened'. Symptom is iPhone-specific: the very first tap on any
of the Host / Nickname / Password text boxes in the connect form
fails to focus + open the keyboard. The second tap on the same
field works.
Two distinct causes, both addressed:
apps/chanora_flutter/lib/main.dart
The connect form lives inside a SingleChildScrollView. Flutter
on iOS has a long-standing issue (flutter#19027) where the
enclosing Scrollable's gesture-arena participant absorbs the
first tap as a possible scroll-intent, leaving the TextField
unfocused; the second tap reaches the field because the
scrollable has already declined to handle a drag.
Fix: _ConnectForm converted from StatelessWidget to
StatefulWidget so it can own FocusNodes for the three text
fields. Each TextField gains:
* focusNode: <its own FocusNode>
* onTap: () => focusNode.requestFocus()
— forces focus on tap-down regardless of arena outcome
* textInputAction: TextInputAction.next (host, nick) /
TextInputAction.done (password) for return-key flow
* autocorrect: false, enableSuggestions: false
— these are server-host / nickname / password fields, the
iOS auto-correct + suggestion bar is wrong for all three.
Also set keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior
.onDrag on the SingleChildScrollView so the keyboard hides
when the user starts scrolling the bookmark list below.
apps/chanora_flutter/ios/Runner/AppDelegate.swift
AVAudioSession.sharedInstance().requestRecordPermission was
fired synchronously from didFinishLaunchingWithOptions. The
permission alert can race with iOS's text-input subsystem
initialisation: if the alert appears before the keyboard
layer finishes wiring up, subsequent text-field focus
requests are dropped silently.
Fix: DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) to
defer the permission request until ~1 s after the app shell
is on screen. Long enough for iOS's text-input layer to fully
initialise; short enough that the user reads the prompt
before tapping a field.
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
flutter build ios --release --no-codesign: 26.8 s clean
(Runner.app 29.8 MB).
72 lines
3.1 KiB
Swift
72 lines
3.1 KiB
Swift
import UIKit
|
|
import Flutter
|
|
import AVFoundation
|
|
|
|
@main
|
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
|
override func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
) -> Bool {
|
|
// Configure the iOS AVAudioSession for voice chat BEFORE Flutter
|
|
// starts the audio engine. The category + mode combination tells
|
|
// iOS to:
|
|
// * route via the receiver/speaker like a phone call
|
|
// (`.playAndRecord` + `.voiceChat`)
|
|
// * engage hardware AEC / NS where the device supports it
|
|
// * default the speaker output (so the user doesn't have to
|
|
// hold the phone to their ear)
|
|
// * permit Bluetooth headsets (so AirPods et al. just work)
|
|
//
|
|
// SRS-197 covers the iOS audio routing contract; this is the
|
|
// matching iOS-side implementation. Failures are logged but do
|
|
// not block app launch — the audio engine will still come up,
|
|
// just at the iOS default playback route.
|
|
do {
|
|
let session = AVAudioSession.sharedInstance()
|
|
try session.setCategory(
|
|
.playAndRecord,
|
|
mode: .voiceChat,
|
|
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]
|
|
)
|
|
try session.setActive(true, options: [])
|
|
NSLog("chanora_flutter: AVAudioSession configured (playAndRecord/voiceChat)")
|
|
} catch {
|
|
NSLog("chanora_flutter: AVAudioSession setup failed: \(error)")
|
|
}
|
|
|
|
// Request microphone access on first launch rather than waiting
|
|
// for the user's first voice-channel join. The latter is
|
|
// surprising: the user has only tapped "connect to server" and
|
|
// suddenly iOS pops the permission prompt because joining a
|
|
// text channel happens to trigger audio engine startup. Asking
|
|
// up-front matches user expectations for a voice-chat client.
|
|
//
|
|
// The request is asynchronous and non-blocking. If the user
|
|
// denies, voice_join will surface a clearer error later when
|
|
// the audio engine fails to open the input device. The
|
|
// permission state is cached by iOS so subsequent launches
|
|
// skip the prompt.
|
|
//
|
|
// Deferred ~1 s so iOS finishes initialising the keyboard /
|
|
// text-input subsystem before the permission alert appears.
|
|
// Firing the alert too early steals focus from the not-yet-
|
|
// ready text-input layer, with the symptom that the first tap
|
|
// on a TextField does nothing (the second tap works because
|
|
// by then iOS has caught up). DispatchQueue.main.asyncAfter
|
|
// keeps everything on the main thread; the permission API
|
|
// itself must be called there too.
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
NSLog("chanora_flutter: microphone permission granted=\(granted)")
|
|
}
|
|
}
|
|
|
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
}
|
|
|
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
|
}
|
|
}
|